forked from nvim-mini/mini.nvim
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmove.lua
More file actions
500 lines (417 loc) · 18.9 KB
/
move.lua
File metadata and controls
500 lines (417 loc) · 18.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
--- *mini.move* Move any selection in any direction
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
--- Features:
--- - Works in two modes:
--- - Visual mode. Select text (charwise with |v|, linewise with |V|, and
--- blockwise with |CTRL-V|) and press customizable mapping to move in
--- all four directions (left, right, down, up). It keeps Visual mode.
--- - Normal mode. Press customizable mapping to move current line in all
--- four directions (left, right, down, up).
--- - Special handling of linewise movement:
--- - Vertical movement gets reindented with |=|.
--- - Horizontal movement is improved indent/dedent with |>| / |<|.
--- - Cursor moves along with selection.
---
--- - Provides both mappings and Lua functions for motions. See
--- |MiniMove.move_selection()| and |MiniMove.move_line()|.
---
--- - Respects |v:count|. Movement mappings can be preceded by a number which
--- multiplies command effect.
---
--- - All consecutive moves (regardless of direction) can be undone by a single |u|.
---
--- - Respects preferred column for vertical movement. It will vertically move
--- selection as how cursor is moving (not strictly vertically if target
--- column is not present in target line).
---
--- Notes:
--- - Doesn't allow moving selection outside of current lines (by design).
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.move').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniMove`
--- which you can use for scripting or manually (with `:lua MiniMove.*`).
---
--- See |MiniMove.config| for available config settings.
---
--- You can override runtime config settings (but not `config.mappings`) locally
--- to buffer inside `vim.b.minimove_config` which should have same structure
--- as `MiniMove.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - [matze/vim-move](https://github.com/matze/vim-move):
--- - Doesn't support vertical movement of charwise and blockwise selections.
--- While 'mini.move' does.
--- - Doesn't support horizontal movement of current line in favor of
--- horizontal movement of current character. While 'mini.move' supports
--- horizontal movement of current line and doesn't support such movement
--- of current character.
--- - Has extra functionality for certain moves (like move by half page).
--- While 'mini.move' does not (by design).
--- - [booperlv/nvim-gomove](https://github.com/booperlv/nvim-gomove):
--- - Doesn't support movement in charwise visual selection.
--- While 'mini.move' does.
--- - Has extra functionality beyond moving text, like duplication.
--- While 'mini.move' concentrates only on moving functionality.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minimove_disable` (globally) or `vim.b.minimove_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 MiniMove
---@alias __move_direction string One of "left", "down", "up", "right".
---@alias __move_opts table|nil Options. Same structure as `options` in |MiniMove.config|
--- (with its values as defaults) plus these allowed extra fields:
--- - <n_times> (number) - number of times to try to make a move.
--- Default: |v:count1|.
---@diagnostic disable:undefined-field
-- Module definition ==========================================================
local MiniMove = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniMove.config|.
---
---@usage >lua
--- require('mini.move').setup() -- use default config
--- -- OR
--- require('mini.move').setup({}) -- replace {} with your config table
--- <
MiniMove.setup = function(config)
-- Export module
_G.MiniMove = MiniMove
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Mappings ~
---
--- Other possible choices of mappings: >lua
---
--- -- `HJKL` for moving visual selection (overrides H, L, J in Visual mode)
--- require('mini.move').setup({
--- mappings = {
--- left = 'H',
--- right = 'L',
--- down = 'J',
--- up = 'K',
--- }
--- })
---
--- -- Shift + arrows
--- require('mini.move').setup({
--- mappings = {
--- left = '<S-left>',
--- right = '<S-right>',
--- down = '<S-down>',
--- up = '<S-up>',
---
--- line_left = '<S-left>',
--- line_right = '<S-right>',
--- line_down = '<S-down>',
--- line_up = '<S-up>',
--- }
--- })
--- <
MiniMove.config = {
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Move visual selection in Visual mode. Defaults are Alt (Meta) + hjkl.
left = '<M-h>',
right = '<M-l>',
down = '<M-j>',
up = '<M-k>',
-- Move current line in Normal mode
line_left = '<M-h>',
line_right = '<M-l>',
line_down = '<M-j>',
line_up = '<M-k>',
},
-- Options which control moving behavior
options = {
-- Automatically reindent selection during linewise vertical move
reindent_linewise = true,
},
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Move visually selected region in any direction within present lines
---
--- Main function powering visual selection move in Visual mode.
---
--- Notes:
--- - Vertical movement in linewise mode is followed up by reindent with |v_=|.
--- - Horizontal movement in linewise mode is same as |v_<| and |v_>|.
---
---@param direction __move_direction
---@param opts __move_opts
MiniMove.move_selection = function(direction, opts)
if H.is_disabled() or not vim.o.modifiable then return end
opts = vim.tbl_deep_extend('force', H.get_config().options, opts or {})
-- This could have been a one-line expression mappings, but there are issues:
-- - Initial yanking modifies some register. Not critical, but also not good.
-- - Doesn't work at movement edges (first line for `K`, etc.). See
-- https://github.com/vim/vim/issues/11786
-- - Results into each movement being a separate undo block, which is
-- inconvenient with several back-to-back movements.
local cur_mode = vim.fn.mode()
-- Act only inside visual mode
if not (cur_mode == 'v' or cur_mode == 'V' or cur_mode == '\22') then return end
-- Define common predicates
local dir_type = (direction == 'up' or direction == 'down') and 'vert' or 'hori'
local is_linewise = cur_mode == 'V'
-- Make early return in small buffer
if vim.api.nvim_buf_line_count(0) == 1 then
if is_linewise and dir_type == 'vert' then return end
if not is_linewise and vim.fn.getline(1):len() == 0 then return end
end
-- Cache useful data because it will be reset when executing commands
local n_times = opts.n_times or vim.v.count1
local ref_curpos, ref_last_col = vim.fn.getcurpos(), vim.fn.col('$')
local is_cursor_on_selection_start = vim.fn.line('.') < vim.fn.line('v')
-- Determine if previous action was this type of move
local is_moving = vim.deep_equal(H.state, H.get_move_state())
if not is_moving then H.curswant = nil end
-- Allow undo of consecutive moves at once (direction doesn't matter)
local cmd = H.make_cmd_normal(is_moving)
-- Treat horizontal linewise movement specially
if is_linewise and dir_type == 'hori' then
-- Use indentation as horizontal movement for linewise selection
cmd(n_times .. H.indent_keys[direction] .. 'gv')
-- Make cursor move along selection
H.correct_cursor_col(ref_curpos, ref_last_col)
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
return
end
-- Temporarily ensure possibility to put cursor just after line end.
-- This allows a more intuitive cursor positioning from and to end of line.
-- NOTE: somehow, this should be done before initial cut to take effect.
local cache_virtualedit = vim.o.virtualedit
if not cache_virtualedit:find('all') then vim.o.virtualedit = 'onemore' end
-- Cut selection while saving caching register
local cache_z_reg = vim.fn.getreginfo('z')
-- - Don't use `"zx` directly to not affect registers 1-9
cmd('"zygv"_x')
-- Detect edge selection: last line(s) for vertical and last character(s)
-- for horizontal. At this point (after cutting selection) cursor is on the
-- edge which can happen in two cases:
-- - Move second to last selection towards edge (like in 'abc' move 'b'
-- to right or second to last line down).
-- - Move edge selection away from edge (like in 'abc' move 'c' to left
-- or last line up).
-- Use condition that removed selection was further than current cursor
-- to distinguish between two cases.
local is_edge_selection_hori = dir_type == 'hori' and vim.fn.col('.') < vim.fn.col("'<")
local is_edge_selection_vert = dir_type == 'vert' and vim.fn.line('.') < vim.fn.line("'<")
local is_edge_selection = is_edge_selection_hori or is_edge_selection_vert
-- Use `p` as paste key instead of `P` in cases which might require moving
-- selection to place which is unreachable with `P`: right to be line end
-- and down to be last line. NOTE: temporary `virtualedit=onemore` solves
-- this only for horizontal movement, but not for vertical.
local can_go_overline = not is_linewise and direction == 'right'
local can_go_overbuf = is_linewise and direction == 'down'
local paste_key = (can_go_overline or can_go_overbuf) and 'p' or 'P'
-- Restore `curswant` to try move cursor to initial column (just like
-- default `hjkl` moves)
if dir_type == 'vert' then H.set_curswant(H.curswant) end
-- Possibly reduce number of moves by one to not overshoot move
local n = n_times - ((paste_key == 'p' or is_edge_selection) and 1 or 0)
-- Don't allow movement past last line of block selection (any part)
if cur_mode == '\22' and direction == 'down' and vim.fn.line('$') == vim.fn.line("'>") then n = 0 end
-- Move cursor
if n > 0 then cmd(n .. H.move_keys[direction]) end
-- Save curswant. Correct for one less move when using `p` as paste.
H.curswant = H.get_curswant() + ((direction == 'right' and paste_key == 'p') and 1 or 0)
-- Open just enough folds (but not in linewise mode, as it allows moving
-- past folds)
if not is_linewise then cmd('zv') end
-- Paste
cmd('"z' .. paste_key)
-- Select newly moved region. Another way is to use something like `gvhoho`
-- but it doesn't work well with selections spanning several lines.
cmd('`[1v')
-- Do extra in case of linewise selection
if is_linewise then
-- Reindent linewise selection if `=` can do that.
-- NOTE: this sometimes doesn't work well with folds (and probably
-- `foldmethod=indent`) and linewise mode because it recomputes folds after
-- that and the whole "move past fold" doesn't work.
if opts.reindent_linewise and dir_type == 'vert' and vim.o.equalprg == '' then cmd('=gv') end
-- Move cursor along the selection. NOTE: do this *after* reindent to
-- account for its effect.
-- - Ensure that cursor is on the right side of selection
if is_cursor_on_selection_start then cmd('o') end
H.correct_cursor_col(ref_curpos, ref_last_col)
end
-- Restore intermediate values
vim.fn.setreg('z', cache_z_reg)
vim.o.virtualedit = cache_virtualedit
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
end
--- Move current line in any direction
---
--- Main function powering current line move in Normal mode.
---
--- Notes:
--- - Vertical movement is followed up by reindent with |v_=|.
--- - Horizontal movement is almost the same as |<<| and |>>| with a different
--- handling of |v:count| (multiplies shift effect instead of modifying that
--- number of lines).
---
---@param direction __move_direction
---@param opts __move_opts
MiniMove.move_line = function(direction, opts)
if H.is_disabled() or not vim.o.modifiable then return end
if vim.api.nvim_buf_line_count(0) == 1 and (direction == 'down' or direction == 'up') then return end
opts = vim.tbl_deep_extend('force', H.get_config().options, opts or {})
-- Determine if previous action was this type of move
local is_moving = vim.deep_equal(H.state, H.get_move_state())
-- Allow undo of consecutive moves at once (direction doesn't matter)
local cmd = H.make_cmd_normal(is_moving)
-- Cache useful data because it will be reset when executing commands
local n_times = opts.n_times or vim.v.count1
local is_last_line_up = direction == 'up' and vim.fn.line('.') == vim.fn.line('$')
local ref_curpos, ref_last_col = vim.fn.getcurpos(), vim.fn.col('$')
if direction == 'left' or direction == 'right' then
-- Use indentation as horizontal movement. Explicitly call `count1` because
-- `<`/`>` use `v:count` to define number of lines.
-- Go to first non-blank at the end.
local key = H.indent_keys[direction]
cmd(string.rep(key .. key, n_times))
-- Make cursor move along selection
H.correct_cursor_col(ref_curpos, ref_last_col)
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
return
end
-- Cut current line while saving caching register
local cache_z_reg = vim.fn.getreginfo('z')
-- - Don't use `"zdd` directly to not affect registers 1-9
cmd('"zyy"_dd')
-- Move cursor
local paste_key = direction == 'up' and 'P' or 'p'
local n = n_times - ((paste_key == 'p' or is_last_line_up) and 1 or 0)
if n > 0 then cmd(n .. H.move_keys[direction]) end
-- Paste
cmd('"z' .. paste_key)
-- Reindent and put cursor on first non-blank
if opts.reindent_linewise and vim.o.equalprg == '' then cmd('==') end
-- Move cursor along the selection. NOTE: do this *after* reindent to
-- account for its effect.
H.correct_cursor_col(ref_curpos, ref_last_col)
-- Restore intermediate values
vim.fn.setreg('z', cache_z_reg)
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniMove.config)
H.move_keys = { left = 'h', down = 'j', up = 'k', right = 'l' }
H.indent_keys = { left = '<', right = '>' }
-- Moving state used to decide when to start new undo block ...
H.state = {
-- ... on buffer change
buf_id = nil,
-- ... on text change
changedtick = nil,
-- ... on cursor move
cursor = nil,
-- ... on mode change
mode = nil,
}
H.curswant = nil
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
H.check_type('config', config, 'table', true)
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
H.check_type('mappings', config.mappings, 'table')
H.check_type('mappings.left', config.mappings.left, 'string')
H.check_type('mappings.down', config.mappings.down, 'string')
H.check_type('mappings.up', config.mappings.up, 'string')
H.check_type('mappings.right', config.mappings.right, 'string')
H.check_type('mappings.line_left', config.mappings.line_left, 'string')
H.check_type('mappings.line_right', config.mappings.line_right, 'string')
H.check_type('mappings.line_down', config.mappings.line_down, 'string')
H.check_type('mappings.line_up', config.mappings.line_up, 'string')
H.check_type('options', config.options, 'table')
H.check_type('options.reindent_linewise', config.options.reindent_linewise, 'boolean')
return config
end
--stylua: ignore
H.apply_config = function(config)
MiniMove.config = config
-- Make mappings
local maps = config.mappings
H.map('x', maps.left, [[<Cmd>lua MiniMove.move_selection('left')<CR>]], { desc = 'Move left' })
H.map('x', maps.right, [[<Cmd>lua MiniMove.move_selection('right')<CR>]], { desc = 'Move right' })
H.map('x', maps.down, [[<Cmd>lua MiniMove.move_selection('down')<CR>]], { desc = 'Move down' })
H.map('x', maps.up, [[<Cmd>lua MiniMove.move_selection('up')<CR>]], { desc = 'Move up' })
H.map('n', maps.line_left, [[<Cmd>lua MiniMove.move_line('left')<CR>]], { desc = 'Move line left' })
H.map('n', maps.line_right, [[<Cmd>lua MiniMove.move_line('right')<CR>]], { desc = 'Move line right' })
H.map('n', maps.line_down, [[<Cmd>lua MiniMove.move_line('down')<CR>]], { desc = 'Move line down' })
H.map('n', maps.line_up, [[<Cmd>lua MiniMove.move_line('up')<CR>]], { desc = 'Move line up' })
end
H.is_disabled = function() return vim.g.minimove_disable == true or vim.b.minimove_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniMove.config, vim.b.minimove_config or {}, config or {})
end
-- Utilities ------------------------------------------------------------------
H.error = function(msg) error('(mini.move) ' .. msg, 0) end
H.check_type = function(name, val, ref, allow_nil)
if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
end
H.map = function(mode, lhs, rhs, opts)
if lhs == '' then return end
opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
vim.keymap.set(mode, lhs, rhs, opts)
end
H.make_cmd_normal = function(include_undojoin)
local normal_command = (include_undojoin and 'undojoin | ' or '') .. 'silent keepjumps normal! '
return function(x)
-- Caching and restoring data on every command is not necessary but leads
-- to a nicer implementation
-- Disable 'mini.bracketed' to avoid unwanted entries to its yank history
local cache_minibracketed_disable = vim.b.minibracketed_disable
local cache_unnamed_register = { points_to = vim.fn.getreginfo('"').points_to }
-- Don't track possible put commands into yank history
vim.b.minibracketed_disable = true
vim.cmd(normal_command .. x)
vim.b.minibracketed_disable = cache_minibracketed_disable
vim.fn.setreg('"', cache_unnamed_register)
end
end
H.get_move_state = function()
return {
buf_id = vim.api.nvim_get_current_buf(),
changedtick = vim.b.changedtick,
cursor = vim.api.nvim_win_get_cursor(0),
mode = vim.fn.mode(),
}
end
H.correct_cursor_col = function(ref_curpos, ref_last_col)
-- Use `ref_curpos = getcurpos()` instead of `vim.api.nvim_win_get_cursor(0)`
-- allows to also account for `virtualedit=all`
local col_diff = vim.fn.col('$') - ref_last_col
local new_col = math.max(ref_curpos[3] + col_diff, 1)
vim.fn.cursor({ vim.fn.line('.'), new_col, ref_curpos[4], ref_curpos[5] + col_diff })
end
H.get_curswant = function() return vim.fn.winsaveview().curswant end
H.set_curswant = function(x)
if x == nil then return end
vim.fn.winrestview({ curswant = x })
end
return MiniMove