forked from nvim-mini/mini.nvim
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbracketed.lua
More file actions
2004 lines (1688 loc) · 80.5 KB
/
bracketed.lua
File metadata and controls
2004 lines (1688 loc) · 80.5 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.bracketed* Go forward/backward with square brackets
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
--- Features:
--- - Configurable Lua functions to go forward/backward to a certain target.
--- Each function can be customized with:
--- - Direction. One of "forward", "backward", "first" (forward starting
--- from first one), "last" (backward starting from last one).
--- - Number of times to go.
--- - Whether to wrap on edges (going forward on last one goes to first).
--- - Some other target specific options.
---
--- - Mappings using square brackets. They are created using configurable
--- target suffix and can be selectively disabled.
---
--- Each mapping supports |[count]|. Mappings are created in Normal mode; for
--- targets which move cursor in current buffer also Visual and
--- Operator-pending (with dot-repeat) modes are supported.
---
--- Using `lower-suffix` and `upper-suffix` (lower and upper case suffix) for
--- a single target the following mappings are created:
--- - `[` + `upper-suffix` : go first.
--- - `[` + `lower-suffix` : go backward.
--- - `]` + `lower-suffix` : go forward.
--- - `]` + `upper-suffix` : go last.
---
--- - Supported targets (for more information see help for corresponding Lua
--- function):
---
--- `Target` `Mappings` `Lua function`
--- Buffer ......................... `[B` `[b` `]b` `]B` .... |MiniBracketed.buffer()|
--- Comment block .................. `[C` `[c` `]c` `]C` .... |MiniBracketed.comment()|
--- Conflict marker ................ `[X` `[x` `]x` `]X` .... |MiniBracketed.conflict()|
--- Diagnostic ..................... `[D` `[d` `]d` `]D` .... |MiniBracketed.diagnostic()|
--- File on disk ................... `[F` `[f` `]f` `]F` .... |MiniBracketed.file()|
--- Indent change .................. `[I` `[i` `]i` `]I` .... |MiniBracketed.indent()|
--- Jump inside current buffer ..... `[J` `[j` `]j` `]J` .... |MiniBracketed.jump()|
--- Location from |location-list| .... `[L` `[l` `]l` `]L` .... |MiniBracketed.location()|
--- Old files ...................... `[O` `[o` `]o` `]O` .... |MiniBracketed.oldfile()|
--- Quickfix entry from |Quickfix| ... `[Q` `[q` `]q` `]Q` .... |MiniBracketed.quickfix()|
--- Tree-sitter node and parents ... `[T` `[t` `]t` `]T` .... |MiniBracketed.treesitter()|
--- Undo from linear history ....... `[U` `[u` `]u` `]U` .... |MiniBracketed.undo()|
--- Window in current tab .......... `[W` `[w` `]w` `]W` .... |MiniBracketed.window()|
--- Yank entry over put region ..... `[Y` `[y` `]y` `]Y` .... |MiniBracketed.yank()|
---
--- Notes:
--- - The `undo` target remaps |u| and |CTRL-R| keys to register undo state
--- after undo and redo respectively. If this conflicts with your setup,
--- either disable `undo` target or make your remaps after calling
--- |MiniBracketed.setup()|. To use `undo` target, remap your undo/redo keys
--- to call |MiniBracketed.register_undo_state()| after the action.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.bracketed').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniBracketed`
--- which you can use for scripting or manually (with `:lua MiniBracketed.*`).
---
--- See |MiniBracketed.config| for available config settings.
---
--- You can override runtime config settings (like target options) locally
--- to buffer inside `vim.b.minibracketed_config` which should have same structure
--- as `MiniBracketed.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - [tpope/vim-unimpaired](https://github.com/tpope/vim-unimpaired):
--- - Supports buffer, conflict, file, location, and quickfix targets mostly
--- via built-in commands (like |:bprevious|, etc.) without configuration.
--- - Supports files from argument list and tags. This module does not.
--- - Doesn't support most other this module's targets (comment, indent, ...).
--- - |mini.indentscope|:
--- - Target |MiniBracketed.indent()| target can go to "first" and "last"
--- indent change. It also can go not only to line with smaller indent,
--- but also bigger or different one.
--- - Mappings from 'mini.indentscope' have more flexibility in computation of
--- indent scope, like how to treat empty lines near border or whether to
--- compute indent at cursor.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minibracketed_disable` (globally) or
--- `vim.b.minibracketed_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.
---@tag MiniBracketed
---@diagnostic disable:luadoc-miss-type-name
---@alias __bracketed_direction string One of "first", "backward", "forward", "last".
---@alias __bracketed_add_to_jumplist - <add_to_jumplist> (`boolean`) - Whether to add current position to jumplist.
--- Default: `false`.
---@alias __bracketed_opts table|nil Options. A table with fields:
--- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
--- - <wrap> `(boolean)` - Whether to wrap around edges. Default: `true`.
---@diagnostic disable:undefined-field
-- Module definition ==========================================================
local MiniBracketed = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniBracketed.config|.
---
---@usage >lua
--- require('mini.bracketed').setup() -- use default config
--- -- OR
--- require('mini.bracketed').setup({}) -- replace {} with your config table
--- <
MiniBracketed.setup = function(config)
-- Export module
_G.MiniBracketed = MiniBracketed
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
end
--stylua: ignore
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text Options ~
---
--- Each entry configures target with the same name and can have data configuring
--- mapping suffix and target options.
---
--- Example of configuration: >lua
---
--- require('mini.bracketed').setup({
--- -- Map [N, [n, ]n, ]N for conflict marker like in 'tpope/vim-unimpaired'
--- conflict = { suffix = 'n' },
---
--- -- Make diagnostic advance only by errors
--- diagnostic = { options = { severity = vim.diagnostic.severity.ERROR } },
---
--- -- Disable creation of mappings for `indent` target (for example,
--- -- in favor of ones from |mini.indentscope|)
--- indent = { suffix = '' },
---
--- -- Disable mappings for `window` target in favor of custom ones
--- window = { suffix = '' },
--- })
---
--- -- Create custom `window` mappings
--- local map = vim.keymap.set
--- map('n', '<Leader>wH', "<Cmd>lua MiniBracketed.window('first')<CR>")
--- map('n', '<Leader>wh', "<Cmd>lua MiniBracketed.window('backward')<CR>")
--- map('n', '<Leader>wl', "<Cmd>lua MiniBracketed.window('forward')<CR>")
--- map('n', '<Leader>wL', "<Cmd>lua MiniBracketed.window('last')<CR>")
--- <
--- ## Suffix ~
---
--- The `suffix` key is used to create target mappings.
---
--- Supply empty string to disable mapping creation for that particular target.
--- To create a completely different mapping (like with |<Leader>|) use target
--- function manually.
---
--- Using `lower-suffix` and `upper-suffix` (lower and upper case suffix) for
--- a single target the following mappings are created:
--- - `[` + `upper-suffix` : go first.
--- - `[` + `lower-suffix` : go backward.
--- - `]` + `lower-suffix` : go forward.
--- - `]` + `upper-suffix` : go last.
---
--- When supplied with a non-letter, only forward/backward mappings are created.
---
--- ## Options ~
---
--- The `options` key is directly forwarded as `opts` to corresponding Lua function.
MiniBracketed.config = {
-- First-level elements are tables describing behavior of a target:
--
-- - <suffix> - single character suffix. Used after `[` / `]` in mappings.
-- For example, with `b` creates `[B`, `[b`, `]b`, `]B` mappings.
-- Supply empty string `''` to not create mappings.
--
-- - <options> - table overriding target options.
--
-- See `:h MiniBracketed.config` for more info.
buffer = { suffix = 'b', options = {} },
comment = { suffix = 'c', options = {} },
conflict = { suffix = 'x', options = {} },
diagnostic = { suffix = 'd', options = {} },
file = { suffix = 'f', options = {} },
indent = { suffix = 'i', options = {} },
jump = { suffix = 'j', options = {} },
location = { suffix = 'l', options = {} },
oldfile = { suffix = 'o', options = {} },
quickfix = { suffix = 'q', options = {} },
treesitter = { suffix = 't', options = {} },
undo = { suffix = 'u', options = {} },
window = { suffix = 'w', options = {} },
yank = { suffix = 'y', options = {} },
}
--minidoc_afterlines_end
--- Listed buffer
---
--- Go to next/previous listed buffer. Order by their number (see |bufnr()|).
---
--- Direction "forward" increases number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.buffer = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'buffer')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().buffer.options, opts or {})
-- Define iterator that traverses all valid listed buffers
-- (should be same as `:bnext` / `:bprev`)
local buf_list = vim.api.nvim_list_bufs()
local is_listed = function(buf_id) return vim.api.nvim_buf_is_valid(buf_id) and vim.bo[buf_id].buflisted end
local iterator = {}
iterator.next = function(buf_id)
for id = buf_id + 1, buf_list[#buf_list] do
if is_listed(id) then return id end
end
end
iterator.prev = function(buf_id)
for id = buf_id - 1, buf_list[1], -1 do
if is_listed(id) then return id end
end
end
iterator.state = vim.api.nvim_get_current_buf()
iterator.start_edge = buf_list[1] - 1
iterator.end_edge = buf_list[#buf_list] + 1
-- Iterate
local res_buf_id = MiniBracketed.advance(iterator, direction, opts)
if res_buf_id == iterator.state then return end
-- Apply
vim.api.nvim_set_current_buf(res_buf_id)
end
--- Comment block
---
--- Go to next/previous comment block. Only linewise comments using
--- 'commentsring' are recognized.
---
--- Direction "forward" increases line number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
--- __bracketed_add_to_jumplist
--- - <block_side> `(string)` - which side of comment block to use. One of
--- "near" (default; use nearest side), "start" (use first line), "end"
--- (use last line), "both" (use both first and last lines).
MiniBracketed.comment = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'comment')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, block_side = 'near', n_times = vim.v.count1, wrap = true },
H.get_config().comment.options,
opts or {}
)
-- Compute loop data to traverse target commented lines in current buffer
local is_commented = H.make_comment_checker()
if is_commented == nil then return end
local predicate = ({
near = function(_, cur, _, recent) return cur and not recent end,
start = function(above, cur, _, _) return cur and not above end,
['end'] = function(_, cur, below, _) return cur and not below end,
both = function(above, cur, below, _) return cur and not (above and below) end,
})[opts.block_side]
if predicate == nil then return end
-- Define iterator
local iterator = {}
local n_lines = vim.api.nvim_buf_line_count(0)
iterator.next = function(line_num)
local above, cur = is_commented(line_num), is_commented(line_num + 1)
for lnum = line_num + 1, n_lines do
local below = is_commented(lnum + 1)
if predicate(above, cur, below, above) then return lnum end
above, cur = cur, below
end
end
iterator.prev = function(line_num)
local cur, below = is_commented(line_num - 1), is_commented(line_num)
for lnum = line_num - 1, 1, -1 do
local above = is_commented(lnum - 1)
if predicate(above, cur, below, below) then return lnum end
below, cur = cur, above
end
end
iterator.state = vim.fn.line('.')
iterator.start_edge = 0
iterator.end_edge = n_lines + 1
-- Iterate
local res_line_num = MiniBracketed.advance(iterator, direction, opts)
local is_outside = res_line_num <= 0 or n_lines < res_line_num
if res_line_num == nil or res_line_num == iterator.state or is_outside then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply. Open just enough folds and put cursor on first non-blank.
H.set_cursor(res_line_num, 0)
vim.cmd('normal! zv^')
end
--- Git conflict marker
---
--- Go to next/previous lines containing Git conflict marker. That is, if it
--- starts with "<<<<<<< ", ">>>>>>> ", or is "=======".
---
--- Direction "forward" increases line number, "backward" - decreases.
---
--- Notes:
--- - Using this target in Operator-pending mode allows the following approach
--- at resolving merge conflicts:
--- - Place cursor on `=======` line.
--- - Execute one of these: `d]x[xdd` (choose upper part) or
--- `d[x]xdd` (choose lower part).
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
--- __bracketed_add_to_jumplist
MiniBracketed.conflict = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'conflict')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, n_times = vim.v.count1, wrap = true },
H.get_config().conflict.options,
opts or {}
)
-- Define iterator that traverses all conflict markers in current buffer
local n_lines = vim.api.nvim_buf_line_count(0)
local iterator = {}
iterator.next = function(line_num)
for lnum = line_num + 1, n_lines do
if H.is_conflict_mark(lnum) then return lnum end
end
end
iterator.prev = function(line_num)
for lnum = line_num - 1, 1, -1 do
if H.is_conflict_mark(lnum) then return lnum end
end
end
iterator.state = vim.fn.line('.')
iterator.start_edge = 0
iterator.end_edge = n_lines + 1
-- Iterate
local res_line_num = MiniBracketed.advance(iterator, direction, opts)
local is_outside = res_line_num <= 0 or n_lines < res_line_num
if res_line_num == nil or res_line_num == iterator.state or is_outside then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply. Open just enough folds and put cursor on first non-blank.
H.set_cursor(res_line_num, 0)
vim.cmd('normal! zv^')
end
--- Diagnostic
---
--- Go to next/previous diagnostic. This is mostly similar to built-in
--- |vim.diagnostic.jump()| (on Neovim<0.11 it is |vim.diagnostic.goto_next()| and
--- |vim.diagnostic.goto_prev()|) which has an interface and behavior
--- consistent with other methods of the module.
---
--- Direction "forward" increases line number, "backward" - decreases.
---
--- Notes:
--- - Using `severity` option, this target can be used in mappings like "go to
--- next/previous error" (), etc. Using code similar to this: >lua
---
--- local severity_error = vim.diagnostic.severity.ERROR
--- -- Use these inside custom mappings
--- MiniBracketed.diagnostic('forward', { severity = severity_error })
--- MiniBracketed.diagnostic('backward', { severity = severity_error })
--- <
---@param direction __bracketed_direction
---@param opts __bracketed_opts
--- - <float> `(boolean|table)` - control floating window after movement.
--- For available values see |vim.diagnostic.goto_next()|.
--- - <severity> `(string|table)` - which severity to use.
--- For available values see |diagnostic-severity|.
MiniBracketed.diagnostic = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'diagnostic')
local default_opts = { float = vim.diagnostic.config().float, n_times = vim.v.count1, severity = nil, wrap = true }
local buf_id = vim.api.nvim_get_current_buf()
if vim.is_callable(default_opts.float) then default_opts.float = default_opts.float(nil, buf_id) end
opts = vim.tbl_extend('force', default_opts, H.get_config().diagnostic.options, opts or {})
-- Define iterator that traverses all diagnostic entries in current buffer
local to_cursor_pos = function(pos) return { pos[1] + 1, pos[2] } end
local pos_field = vim.fn.has('nvim-0.11') == 1 and 'pos' or 'cursor_position'
local iterator = {}
iterator.next = function(position)
local next_opts = { [pos_field] = to_cursor_pos(position), severity = opts.severity, wrap = false }
local next = vim.diagnostic.get_next(next_opts)
if next == nil then return end
return { next.lnum, next.col }
end
iterator.prev = function(position)
local prev_opts = { [pos_field] = to_cursor_pos(position), severity = opts.severity, wrap = false }
local prev = vim.diagnostic.get_prev(prev_opts)
if prev == nil then return end
return { prev.lnum, prev.col }
end
-- - Define states with zero-based indexing as used in `vim.diagnostic`.
-- - Go outside of proper buffer position for `start_edge` and `end_edge` to
-- correctly spot diagnostic entry right and start and end of buffer.
local cursor_pos = vim.api.nvim_win_get_cursor(0)
iterator.state = { cursor_pos[1] - 1, cursor_pos[2] }
iterator.start_edge = { 0, -1 }
local last_line = vim.api.nvim_buf_line_count(0)
iterator.end_edge = { last_line - 1, vim.fn.col({ last_line, '$' }) - 1 }
-- Iterate
local res_pos = MiniBracketed.advance(iterator, direction, opts)
if res_pos == nil or res_pos == iterator.state then return end
-- Apply. Use built-in jump with offsetted cursor position to make it respect
-- `vim.diagnostic.config()`.
H.diagnostic_jump({ res_pos[1] + 1, res_pos[2] - 1 }, opts.float, opts.severity)
end
--- File on disk
---
--- Go to next/previous file on disk alphabetically. Files are taken from
--- directory of file in current buffer (or current working directory if buffer
--- doesn't contain a readable file). Only first-level files are used, i.e. it
--- doesn't go inside subdirectories.
---
--- Direction "forward" goes forward alphabetically, "backward" - backward.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.file = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'file')
opts = vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().file.options, opts or {})
-- Get file data
local file_data = H.get_file_data()
if file_data == nil then return end
local file_basenames, directory = file_data.file_basenames, file_data.directory
-- Define iterator that traverses all found files
local iterator = {}
local n_files = #file_basenames
iterator.next = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return 1 end
if n_files <= ind then return end
return ind + 1
end
iterator.prev = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return n_files end
if ind <= 1 then return end
return ind - 1
end
-- - Find filename array index of current buffer
local cur_basename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t')
local cur_basename_ind
if cur_basename ~= '' then
for i, f in ipairs(file_basenames) do
if cur_basename == f then
cur_basename_ind = i
break
end
end
end
iterator.state = cur_basename_ind
iterator.start_edge = 0
iterator.end_edge = n_files + 1
-- Iterate
local res_ind = MiniBracketed.advance(iterator, direction, opts)
if res_ind == iterator.state then return end
-- Apply. Open target_path.
local path_sep = package.config:sub(1, 1)
local target_path = directory .. path_sep .. file_basenames[res_ind]
H.edit(target_path)
end
--- Indent change
---
--- Go to next/previous line with different indent (see |indent()|).
--- Can be used to go to lines with smaller, bigger, or different indent.
---
--- Notes:
--- - Directions "first" and "last" work differently from most other targets
--- for performance reasons. They are essentially "backward" and "forward"
--- with very big `n_times` option.
--- - For similar reasons, `wrap` is not supported.
--- - Blank line inherit indent from near non-blank line in direction of movement.
---
--- Direction "forward" increases line number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts table|nil Options. A table with fields:
--- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
--- __bracketed_add_to_jumplist
--- - <change_type> `(string)` - which type of indent change to use.
--- One of "less" (default; smaller indent), "more" (bigger indent),
--- "diff" (different indent).
MiniBracketed.indent = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'indent')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, change_type = 'less', n_times = vim.v.count1 },
H.get_config().indent.options,
opts or {}
)
opts.wrap = false
if direction == 'first' then
-- For some reason using `n_times = math.huge` leads to infinite loop
direction, opts.n_times = 'backward', vim.api.nvim_buf_line_count(0) + 1
end
if direction == 'last' then
direction, opts.n_times = 'forward', vim.api.nvim_buf_line_count(0) + 1
end
-- Compute loop data to traverse target commented lines in current buffer
local predicate = ({
less = function(new, cur) return new < cur or cur == 0 end,
more = function(new, cur) return new > cur end,
diff = function(new, cur) return new ~= cur end,
})[opts.change_type]
if predicate == nil then return end
-- Define iterator
local iterator = {}
iterator.next = function(cur_lnum)
-- Correctly process empty current line
cur_lnum = vim.fn.nextnonblank(cur_lnum)
local cur_indent = vim.fn.indent(cur_lnum)
local new_lnum, new_indent = cur_lnum, cur_indent
-- Check with `new_lnum > 0` because `nextnonblank()` returns -1 if line is
-- outside of line range
while new_lnum > 0 do
new_indent = vim.fn.indent(new_lnum)
if predicate(new_indent, cur_indent) then return new_lnum end
new_lnum = vim.fn.nextnonblank(new_lnum + 1)
end
end
iterator.prev = function(cur_lnum)
cur_lnum = vim.fn.prevnonblank(cur_lnum)
local cur_indent = vim.fn.indent(cur_lnum)
local new_lnum, new_indent = cur_lnum, cur_indent
while new_lnum > 0 do
new_indent = vim.fn.indent(new_lnum)
if predicate(new_indent, cur_indent) then return new_lnum end
new_lnum = vim.fn.prevnonblank(new_lnum - 1)
end
end
-- - Don't add first and last states as there is no wrapping around edges
iterator.state = vim.fn.line('.')
-- Iterate
local res_line_num = MiniBracketed.advance(iterator, direction, opts)
if res_line_num == nil or res_line_num == iterator.state then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply. Open just enough folds and put cursor on first non-blank.
H.set_cursor(res_line_num, 0)
vim.cmd('normal! zv^')
end
--- Jump inside current buffer
---
--- Go to next/previous jump from |jumplist| which is inside current buffer.
---
--- Notes:
--- - There are no Visual mode mappings due to implementation problems.
---
--- Direction "forward" increases jump number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.jump = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'jump')
opts = vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().jump.options, opts or {})
-- Define iterator that traverses all jumplist entries inside current buffer
local cur_buf_id = vim.api.nvim_get_current_buf()
local jump_list, cur_jump_num = unpack(vim.fn.getjumplist())
local n_list = #jump_list
if n_list == 0 then return end
-- - Correct for zero-based indexing
cur_jump_num = cur_jump_num + 1
local iterator = {}
local is_jump_num_from_current_buffer = function(jump_num)
local jump_entry = jump_list[jump_num]
if jump_entry == nil then return end
return jump_entry.bufnr == cur_buf_id
end
iterator.next = function(jump_num)
for num = jump_num + 1, n_list do
if is_jump_num_from_current_buffer(num) then return num end
end
end
iterator.prev = function(jump_num)
for num = jump_num - 1, 1, -1 do
if is_jump_num_from_current_buffer(num) then return num end
end
end
iterator.state = cur_jump_num
iterator.start_edge = 0
iterator.end_edge = n_list + 1
-- Iterate
local res_jump_num = MiniBracketed.advance(iterator, direction, opts)
if res_jump_num == nil or jump_list[res_jump_num] == nil then return end
-- Apply. Make jump. Allow jumping to current jump entry as it might be
-- different from current cursor position.
H.make_jump(jump_list, cur_jump_num, res_jump_num)
end
--- Location from location list
---
--- Go to next/previous location from |location-list|. This is similar to
--- |:lfirst|, |:lprevious|, |:lnext|, and |:llast| but with support of
--- wrapping around edges and |[count]| for "first"/"last" direction.
---
--- Direction "forward" increases location number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.location = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'location')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().location.options, opts or {})
H.qf_loc_implementation('location', direction, opts)
end
--- Old files from previous and current sessions
---
--- Go to older/newer readable file either from previous session (see |v:oldfiles|)
--- or the current one (updated automatically after |MiniBracketed.setup()| call).
---
--- Direction "forward" goes to more recent files, "backward" - to older.
---
--- Notes:
--- - In current session it tracks only normal buffers (see |'buftype'|) for
--- some readable file.
--- - No new file is tracked when advancing this target. Only after buffer
--- change is done not through this target (like with |MiniBracketed.buffer()|),
--- it updates recency of last advanced and new buffers.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.oldfile = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'oldfile')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().oldfile.options, opts or {})
-- Define iterator that traverses all old files
local cur_path = vim.api.nvim_buf_get_name(0)
H.oldfile_normalize()
local oldfile_arr = H.oldfile_get_array()
local n_oldfiles = #oldfile_arr
local iterator = {}
iterator.next = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return 1 end
if n_oldfiles <= ind then return end
return ind + 1
end
iterator.prev = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return n_oldfiles end
if ind <= 1 then return end
return ind - 1
end
iterator.state = H.cache.oldfile.recency[cur_path]
iterator.start_edge = 0
iterator.end_edge = n_oldfiles + 1
-- Iterate
local res_arr_ind = MiniBracketed.advance(iterator, direction, opts)
if res_arr_ind == nil or res_arr_ind == iterator.state then return end
-- Apply. Edit file at path while marking it not for tracking.
H.cache.oldfile.is_advancing = true
H.edit(oldfile_arr[res_arr_ind])
end
--- Quickfix from quickfix list
---
--- Go to next/previous entry from |quickfix| list. This is similar to
--- |:cfirst|, |:cprevious|, |:cnext|, and |:clast| but with support of
--- wrapping around edges and |[count]| for "first"/"last" direction.
---
--- Direction "forward" increases location number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.quickfix = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'quickfix')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().quickfix.options, opts or {})
H.qf_loc_implementation('quickfix', direction, opts)
end
--- Tree-sitter node
---
--- Go to end/start of current tree-sitter node and its parents (except root).
---
--- Notes:
--- - Requires loaded tree-sitter parser in the current buffer.
--- - Directions "first" and "last" work differently from most other targets
--- for performance reasons. They are essentially "backward" and "forward"
--- with very big `n_times` option.
--- - For similar reasons, `wrap` is not supported.
---
--- Direction "forward" moves cursor forward to node's end, "backward" - backward
--- to node's start.
---
---@param direction __bracketed_direction
---@param opts table|nil Options. A table with fields:
--- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
--- __bracketed_add_to_jumplist
MiniBracketed.treesitter = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'treesitter')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, n_times = vim.v.count1 },
H.get_config().treesitter.options,
opts or {}
)
opts.wrap = false
if direction == 'first' then
direction, opts.n_times = 'backward', math.huge
end
if direction == 'last' then
direction, opts.n_times = 'forward', math.huge
end
-- Define iterator that traverses current node and its parents (except root)
local is_bad_node = function(node) return node == nil or node:parent() == nil end
local is_after = function(row_new, col_new, row_ref, col_ref)
return row_ref < row_new or (row_ref == row_new and col_ref < col_new)
end
local is_before = function(row_new, col_new, row_ref, col_ref) return is_after(row_ref, col_ref, row_new, col_new) end
local iterator = {}
-- Traverse node and parents until node's end is after current position
iterator.next = function(node_pos)
local node = node_pos.node
if is_bad_node(node) then return nil end
local init_row, init_col = node_pos.pos[1], node_pos.pos[2]
local cur_row, cur_col, cur_node = init_row, init_col, node
repeat
if is_bad_node(cur_node) then break end
cur_row, cur_col = cur_node:end_()
-- Correct for end-exclusiveness
cur_col = cur_col - 1
cur_node = cur_node:parent()
until is_after(cur_row, cur_col, init_row, init_col)
if not is_after(cur_row, cur_col, init_row, init_col) then return end
return { node = cur_node, pos = { cur_row, cur_col } }
end
-- Traverse node and parents until node's start is before current position
iterator.prev = function(node_pos)
local node = node_pos.node
if is_bad_node(node) then return nil end
local init_row, init_col = node_pos.pos[1], node_pos.pos[2]
local cur_row, cur_col, cur_node = init_row, init_col, node
repeat
if is_bad_node(cur_node) then break end
cur_row, cur_col = cur_node:start()
cur_node = cur_node:parent()
until is_before(cur_row, cur_col, init_row, init_col)
if not is_before(cur_row, cur_col, init_row, init_col) then return end
return { node = cur_node, pos = { cur_row, cur_col } }
end
local cur_pos = vim.api.nvim_win_get_cursor(0)
local ok, node = pcall(H.get_treesitter_node, cur_pos[1] - 1, cur_pos[2])
if not ok then
H.error(
'In `treesitter()` target can not find tree-sitter node under cursor.'
.. ' Do you have tree-sitter enabled in current buffer?'
)
end
iterator.state = { pos = { cur_pos[1] - 1, cur_pos[2] }, node = node }
-- Iterate
local res_node_pos = MiniBracketed.advance(iterator, direction, opts)
if res_node_pos == nil then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply
H.set_cursor(res_node_pos.pos[1] + 1, res_node_pos.pos[2])
end
--- Undo along a tracked linear history
---
--- In a nutshell:
--- - Keys |u| and |CTRL-R| (although remapped) can be used as usual, but every
--- their execution new state is recorded in this module's linear undo history.
--- - Advancing this target goes along linear undo history revealing undo states
--- **in order they actually appeared**.
--- - One big difference with built-in methods is that tracked linear history
--- can repeat undo states (not consecutively, though).
---
--- Neovim's default way of managing undo history is through branches (see
--- |undo-branches|). Basically it means that if you undo several changes and then
--- make new ones, it creates new undo branch while usually (see |'undolevels'|)
--- saving previous buffer states in another branch. While there are commands
--- to navigate by time of undo state creation (like |:earlier| and |:later|),
--- there is no intuitive way to cycle through them. Existing |g-| and |g+|
--- cycle through undo states **based on their creation time**, which often
--- gets confusing really guickly in extensively edited buffer.
---
--- This `undo()` target provides a way to cycle through linear undo history
--- **in order states actually appeared**. It does so by registering any new undo
--- states plus every time |MiniBracketed.register_undo_state()| is called. To have
--- more "out of the box" experience, |u| and |CTRL-R| are remapped to call it after
--- they perform their undo/redo.
---
--- Example:
---
--- To show more clearly the difference between advancing this target and using
--- built-in functionality, here is an example:
---
--- - Create undo history in a new buffer (|:new|):
--- - Enter `one two three` text.
--- - Delete first word with `daw` and undo the change with `u`.
--- - Delete second word with `daw` and undo the change with `u`.
--- - Delete third word with `daw` and undo the change with `u`.
---
--- - Now try one of the following (each one after performing previous steps in
--- separate new buffer):
--- - Press `u`. It goes back to empty buffer. Press `<C-R>` twice and it
--- goes to the latest change (`one two`). No way to get to other states
--- (like `two three` or `one three`) with these two keys.
---
--- - Press `g-`. It goes to an empty buffer. Press `g+` 4 times. It cycles
--- through all available undo states **in order they were created**.
---
--- - Finally, press `[u`. It goes back to `one two` - state which was
--- **previously visited** by the user. Another `[u` restores `one two three`.
--- Use `]U` to go to latest visited undo state.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.undo = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'undo')
opts = vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().undo.options, opts or {})
-- Define iterator that traverses undo states in order they appeared
local buf_id = vim.api.nvim_get_current_buf()
H.undo_sync(buf_id, vim.fn.undotree())
local iterator = {}
local buf_history = H.cache.undo[buf_id]
local n = #buf_history
iterator.next = function(id)
if id == nil or n <= id then return end
return id + 1
end
iterator.prev = function(id)
if id == nil or id <= 1 then return end
return id - 1
end
iterator.state = buf_history.current_id
iterator.start_edge = 0
iterator.end_edge = n + 1
-- Iterate
local res_id = MiniBracketed.advance(iterator, direction, opts)
if res_id == nil or res_id == iterator.state then return end
-- Apply. Move to undo state by number while recording current history id
buf_history.is_advancing = true
vim.cmd('undo ' .. buf_history[res_id])
buf_history.current_id = res_id
end
--- Register state for undo target
---
--- Use this function to add current undo state to this module's linear undo
--- history. It is used in |MiniBracketed.setup()| to remap |u| and |CTRL-R| keys
--- to add their new state to linear undo history.
MiniBracketed.register_undo_state = function()
local buf_id = vim.api.nvim_get_current_buf()
local tree = vim.fn.undotree()
-- Synchronize undo history and stop advancing
H.undo_sync(buf_id, tree, false)
-- Append new undo state to linear history
local buf_history = H.cache.undo[buf_id]
H.undo_append_state(buf_history, tree.seq_cur)
buf_history.current_id = #buf_history
end
--- Normal window
---
--- Go to next/previous normal window. Order by their number (see |winnr()|).