1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
5 auto_square_brackets
= false,
15 -- XXX: in Lua 5.2 unpack() was moved into table
16 local unpack
= table.unpack
or unpack
22 local init
, supports
= unpack((require(cwd
..".parkour")))
25 local insert_mode
= false
28 local H
= {M
= {}, T
= {}, O
= {}, I
= {}, A
= {}} -- handler functions
29 local R
= {M
= {}, T
= {}, O
= {}} -- registered handler IDs
30 local B
= {M
= {}, T
= {}, O
= {}} -- bindings for registered handlers
31 local new
= {} -- constructors
33 -- IDs of built-in motions and textobjects used in the plugin (copied from vis.h)
34 local VIS_MOVE_CHAR_PREV
= 16
35 local VIS_MOVE_CHAR_NEXT
= 17
36 local VIS_MOVE_NOP
= 64
37 local VIS_TEXTOBJECT_OUTER_LINE
= 23
39 setmetatable(M
, {__newindex
= function(_
, k
)
40 vis
:message(("%s: Unknown option '%s'"):format(cwd
, k
))
43 setmetatable(M
.lispwords
, {__index
= function(t
, filetype
)
44 if supports
[filetype
] then
45 local recfg
= {__newindex
= function(_
, k
, v
)
46 vis
.events
.subscribe(vis
.events
.WIN_OPEN
, function(win
)
47 if filetype
== win
.syntax
then
48 env
[win
].lispwords
[k
] = v
52 t
[filetype
] = setmetatable({}, recfg
)
57 local function win_map(win
, kind
, key
, action
)
59 win
:map(vis
.modes
.OPERATOR_PENDING
, key
, action
)
60 if not M
.autoselect
then
61 win
:map(vis
.modes
.VISUAL
, key
, action
)
63 elseif kind
== "O" then
64 win
:map(vis
.modes
.NORMAL
, key
, action
)
65 win
:map(vis
.modes
.VISUAL
, key
, action
)
66 elseif kind
== "M" then
67 win
:map(vis
.modes
.NORMAL
, key
, action
)
68 win
:map(vis
.modes
.OPERATOR_PENDING
, key
, action
)
69 win
:map(vis
.modes
.VISUAL
, key
, action
)
73 local function mark_active() return insert_mode
and mark
end
75 local function iprep(func
, win
)
83 local function imap(win
, key
, action
)
84 win
:map(vis
.modes
.INSERT
, key
, iprep(action
, win
))
87 local function rewind_on(action
)
89 local sequence
= "<vis-"..action
..">"
90 if vis
.mode
== vis
.modes
.VISUAL
then
91 sequence
= sequence
.. "<vis-selections-remove-all>"
93 vis
:feedkeys(sequence
)
94 -- force a reparse from the very beginning, as undo/redo can create or remove
95 -- multiple paragraphs in one go and I don't know how much to rewind.
96 env
[vis
.win
].parser
.tree
.rewind(0)
100 H
.A
.undo
= rewind_on("undo")
101 H
.A
.redo
= rewind_on("redo")
103 function H
.A
.select_sexp()
104 vis
.mode
= vis
.modes
.VISUAL
105 vis
:textobject(R
.T
.inner_sexp
)
108 local function vinsert(win
, append
)
109 for selection
in win
:selections_iterator() do
110 selection
.pos
= append
and selection
.range
.finish
or selection
.range
.start
112 vis
.mode
= vis
.modes
.INSERT
115 local function backslash(keys
)
117 local sel_is_escaped
= {}
119 local any_sel_in_code
= false
120 for i
= #win
.selections
, 1, -1 do
121 local selection
= win
.selections
[i
]
122 range
.start
, range
.finish
= selection
.pos
, selection
.pos
123 sel_is_escaped
[i
] = env
[win
].walker
.escaped_at(range
)
124 if not sel_is_escaped
[i
] or not sel_is_escaped
[i
].is_comment
then
125 any_sel_in_code
= true
128 if #keys
< 1 and any_sel_in_code
then vis
:info("Character to escape: ") return -1 end
129 for i
= #win
.selections
, 1, -1 do
130 local selection
= win
.selections
[i
]
131 local pos
= selection
.pos
133 win
.file
:insert(pos
, prefix
..keys
)
134 selection
.pos
= pos
+ #(prefix
..keys
)
135 env
[win
].parser
.tree
.rewind(pos
)
140 local function range_by_pos(win
, pos
)
141 for selection
in win
:selections_iterator() do
142 if selection
.pos
== pos
then
143 return insert_mode
and {start
= pos
, finish
= pos
} or selection
.range
, selection
.number
146 return {start
= pos
, finish
= pos
+ (insert_mode
and 0 or 1)}
149 local function selection_by_pos(win
, pos
)
150 for selection
in win
:selections_iterator() do
151 if selection
.pos
== pos
or selection
.range
and selection
.range
.start
== pos
then
157 local function autoselect_region()
158 vis
.mode
= vis
.modes
.VISUAL
159 vis
:textobject(R
.T
.expand_region
)
164 vis
.events
.subscribe(vis
.events
.WIN_STATUS
, function(win
)
165 if win
~= vis
.win
then return end
166 if not vis_parkour(win
) then return end
167 if last_mode
== vis
.mode
then return end
168 if vis
.mode
== vis
.modes
.NORMAL
and last_mode
== vis
.modes
.INSERT
then
174 vis
.events
.subscribe(vis
.events
.WIN_HIGHLIGHT
, function(win
)
175 if win
~= vis
.win
then return end
176 if not vis_parkour(win
) then return end
177 if vis
.mode
== vis
.modes
.INSERT
and mark
then
178 local m
= win
.file
:mark_get(mark
)
179 local p
= win
.selection
.pos
180 win
:style(win
.STYLE_CURSOR
, math
.min(m
, p
), math
.max(m
, p
) - 1)
184 vis
.events
.subscribe(vis
.events
.WIN_OPEN
, function(win
)
185 if not vis_parkour(win
) then return end
186 local function write(pos
, txt
)
188 return win
.file
:insert(pos
, txt
)
190 local function delete(pos
, len
)
192 return win
.file
:delete(pos
, len
)
194 local function read(base
, len
)
195 if base
and base
>= win
.file
.size
then return end
197 len
= len
or win
.file
.size
198 local more
= base
+ len
< win
.file
.size
199 return win
.file
:content(base
, len
), more
201 local function eol_at(pos
)
202 local sel
= selection_by_pos(win
, pos
)
204 local line_start
= sel
.pos
- sel
.col
+ 1
205 return line_start
+ #win
.file
.lines
[sel
.line
]
207 -- XXX: bad, we have to do ad hoc reads on the file.
208 -- This fixes close_and_newline() and open_next_line() not opening an empty line
209 -- as intended, because this wrapper only worked for `pos` equal to that of an
210 -- existing selection. The underlying library sometimes needs to use it for
211 -- arbitrary positions, though. The same applies to bol_at().
213 local line_end
= win
.file
:content(pos
, pos
+ horizon
):find
"\n"
214 return pos
+ line_end
- 1
217 local function bol_at(pos
)
218 local sel
= selection_by_pos(win
, pos
)
220 return sel
.pos
- sel
.col
+ 1
223 env
[win
] = init(win
.syntax
, read, write, delete
, eol_at
, bol_at
)
224 local consumed_char
= {"\n", " "}
225 for ch
in pairs(env
[win
].parser
.opposite
) do
227 table.insert(consumed_char
, ch
)
230 env
[win
].consumed_char
= "[" .. table.concat(consumed_char
, "%") .. "]"
231 for _
, setup
in pairs(keycfg
) do
232 for kind
, bindings
in pairs(setup
) do
233 if kind
== "imap" then
234 for key
, action
in pairs(bindings
) do
235 imap(win
, key
, action
)
237 elseif kind
== "nmap" then
238 for key
, action
in pairs(bindings
) do
239 win
:map(vis
.modes
.NORMAL
, key
, action
)
241 elseif type(bindings
) == "table" then
242 for key
, action
in pairs(bindings
) do
243 win_map(win
, kind
, key
, action
)
251 win
:map(vis
.modes
.INSERT
, "<Enter>", function() vis
:feedkeys("\n") end)
252 win
:map(vis
.modes
.INSERT
, "\\", backslash
)
254 win
:unmap(vis
.modes
.VISUAL
, "i")
255 win
:unmap(vis
.modes
.VISUAL
, "a")
256 -- make sure there are no mappings that start with i or a,
257 -- otherwise switching from visual straight to insert mode won't work:
258 local bindings
= vis
:mappings(vis
.modes
.VISUAL
)
259 for key
in pairs(bindings
) do
260 if key
:sub(1, 1):match("[ia]") then
261 vis
:unmap(vis
.modes
.VISUAL
, key
)
264 win
:map(vis
.modes
.VISUAL
, "i", function() vinsert(win
) end)
265 win
:map(vis
.modes
.VISUAL
, "a", function() vinsert(win
, true) end)
266 win
:map(vis
.modes
.NORMAL
, "v", autoselect_region
)
268 win
:map(vis
.modes
.VISUAL
, "v", function()
269 vis
:textobject(R
.T
.expand_region
)
271 vis
:command("set expandtab")
274 local function is_comment(t
) return t
.is_comment
end
275 local function startof(t
) return t
.start
end
277 function H
.M
.prev_start(win
, range
)
278 return env
[win
].walker
.start_before(range
, is_comment
)
281 function H
.M
.prev_start_float(win
, range
)
282 -- XXX: ignore the second value:
283 return (env
[win
].walker
.start_float_before(range
))
286 function H
.M
.next_start(win
, range
)
287 return env
[win
].walker
.start_after(range
, is_comment
)
290 function H
.M
.next_start_float(win
, range
)
291 -- XXX: ignore the second value:
292 return (env
[win
].walker
.start_float_after(range
))
295 function H
.M
.prev_finish(win
, range
, _
, exclusive
)
296 local newpos
= env
[win
].walker
.finish_before(range
, is_comment
)
297 return newpos
and newpos
- 1 + exclusive
300 function H
.M
.prev_finish_float(win
, range
, _
, exclusive
)
301 local newpos
= env
[win
].walker
.finish_float_before(range
)
302 return newpos
and newpos
- 1 + exclusive
305 function H
.M
.next_finish(win
, range
, _
, exclusive
)
306 local newpos
= env
[win
].walker
.finish_after(range
, is_comment
)
307 return newpos
and newpos
- 1 + exclusive
310 function H
.M
.next_finish_float(win
, range
, _
, exclusive
)
311 local newpos
= env
[win
].walker
.finish_float_after(range
)
312 return newpos
and newpos
- 1 + exclusive
315 function H
.M
.forward(win
, range
)
316 return env
[win
].walker
.finish_after(range
, is_comment
)
319 function H
.M
.backward(win
, range
)
320 return env
[win
].walker
.start_before(range
, is_comment
)
323 function H
.M
.forward_up(win
, range
, pos
, exclusive
)
324 -- XXX: make splice-killing-forward work even on the closing paren:
325 if exclusive
> 0 then
326 local sexp
= env
[win
].walker
.sexp_at(range
)
327 local closing
= env
[win
].parser
.opposite
[sexp
.d
]
328 if sexp
and sexp
.finish
+ (closing
and 1 - #closing
or 0) == pos
then
329 range
.finish
= range
.finish
- exclusive
332 local newpos
= env
[win
].walker
.finish_up_after(range
)
333 return newpos
and newpos
- 1 + exclusive
336 function H
.M
.forward_down(win
, range
)
337 return env
[win
].walker
.start_down_after(range
, is_comment
)
340 function H
.M
.backward_up(win
, range
)
341 return env
[win
].walker
.start_up_before(range
)
344 function H
.M
.backward_down(win
, range
, _
, exclusive
)
345 local newpos
, list
= env
[win
].walker
.finish_down_before(range
, is_comment
)
346 return newpos
and (list
.is_empty
and newpos
or newpos
- 1 + exclusive
)
349 function H
.M
.beginning_of_sentence(win
, range
)
350 -- XXX: make sure we don't go up if on the closing paren:
351 range
.finish
= range
.start
352 return env
[win
].walker
.anylist_start(range
)
355 function H
.M
.end_of_sentence(win
, range
)
356 -- XXX: make sure delete won't splice when on the closing paren:
357 range
.finish
= range
.start
358 local exclude
= vis
.mode
== vis
.modes
.VISUAL
and 1 or 0
359 local newpos
= env
[win
].walker
.anylist_finish(range
)
360 return newpos
and newpos
- exclude
363 function H
.M
.next_section(win
, range
)
364 local sexp
= env
[win
].walker
.find_after(range
, function(t
) return t
.section
and t
.start
> range
.start
end)
365 return sexp
and sexp
.start
or win
.file
.size
368 function H
.M
.prev_section(win
, range
)
369 local sexp
= env
[win
].walker
.find_before(range
, function(t
) return t
.section
end)
370 return sexp
and sexp
.start
or 0
373 function H
.M
.beginning_of_defun(win
, range
)
374 local sexp
= env
[win
].walker
.paragraph_at(range
)
375 return sexp
and sexp
.start
378 function H
.M
.prev_beginning_of_defun(win
, range
)
379 local sexp
= env
[win
].parser
.tree
.before(range
.start
, startof
, is_comment
)
380 return sexp
and sexp
.start
383 function H
.M
.end_of_defun(win
, range
)
384 local sexp
= env
[win
].walker
.paragraph_at(range
)
385 return sexp
and sexp
.finish
+ (insert_mode
and 1 or 0)
388 function H
.M
.next_beginning_of_defun(win
, range
)
389 local parent
= env
[win
].walker
.paragraph_at(range
)
390 local sexp
= env
[win
].parser
.tree
.after(parent
and parent
.finish
or range
.finish
, startof
, is_comment
)
391 return sexp
and sexp
.start
394 function H
.M
.line_begin(win
, range
)
395 return env
[win
].walker
.prev_start_wrapped(range
)
398 function H
.M
.line_end(win
, range
, _
, exclusive
)
399 -- XXX: make sure we don't go up if on the closing paren:
400 range
.finish
= range
.start
401 return env
[win
].walker
.next_finish_wrapped(range
) - 1 + exclusive
404 -- This motion is only for use with delete
405 function H
.M
.backward_word(win
, range
)
406 -- XXX: ignore the second value:
407 return (env
[win
].walker
.start_float_before(range
, true))
410 -- This motion is only for use with delete
411 function H
.M
.forward_word(win
, range
, _
, exclusive
)
412 local newpos
= env
[win
].walker
.finish_float_after(range
, true)
413 return newpos
and newpos
- 1 + exclusive
416 function H
.M
.mark(win
)
417 return mark
and win
.file
:mark_get(mark
)
420 local function block_textobject(opening
, inner
)
421 return function(win
, range
)
422 local parent
= env
[win
].walker
.list_at(range
, opening
)
423 if not parent
or parent
.is_root
then return end
424 local closing
= env
[win
].parser
.opposite
[parent
.d
]
425 local pstart
= parent
.start
+ (inner
and (parent
.p
and #parent
.p
or 0) + #parent
.d
or 0)
426 local pfinish
= parent
.finish
+ 1 - (inner
and #closing
or 0)
427 return pstart
, pfinish
431 H
.T
.outer_list
= block_textobject()
432 H
.T
.inner_list
= block_textobject(nil, true)
433 H
.T
.outer_sentence
= block_textobject(false)
434 H
.T
.inner_sentence
= block_textobject(false, true)
435 H
.T
.outer_string
= block_textobject('"')
436 H
.T
.inner_string
= block_textobject('"', true)
437 H
.T
.outer_comment
= block_textobject(";")
438 H
.T
.inner_comment
= block_textobject(";", true)
439 H
.T
.outer_round
= block_textobject("(")
440 H
.T
.inner_round
= block_textobject("(", true)
441 H
.T
.outer_square
= block_textobject("[")
442 H
.T
.inner_square
= block_textobject("[", true)
443 H
.T
.outer_curly
= block_textobject("{")
444 H
.T
.inner_curly
= block_textobject("{", true)
446 function H
.T
.outer_sexp(win
, range
)
447 local sexp
= env
[win
].walker
.sexp_at(range
)
449 return sexp
.start
, sexp
.finish
+ 1
453 H
.T
.inner_sexp
= H
.T
.outer_sexp
455 function H
.T
.outer_paragraph(win
, range
)
456 local sexps
= env
[win
].walker
.repl_line_at(range
)
458 return sexps
.start
, sexps
.finish
+ 1
462 function H
.T
.inner_paragraph(win
, range
)
463 local sexp
= env
[win
].walker
.paragraph_at(range
)
464 if sexp
and not sexp
.is_line_comment
then
465 return sexp
.start
, sexp
.finish
+ 1
469 function H
.T
.expand_region(win
, range
)
470 return env
[win
].walker
.wider_than(range
)
473 function H
.T
.outer_line(win
, range
)
474 local sel
= selection_by_pos(win
, range
.start
)
475 if not sel
then return end
476 local line_start
= range
.start
- sel
.col
+ 1
477 local line_finish
= line_start
+ #win
.file
.lines
[sel
.line
]
478 return line_start
, line_finish
+ 1
481 function H
.T
.inner_line(win
, range
, pos
)
482 local sel
= selection_by_pos(win
, range
.start
)
483 if not sel
then return end
484 local _
, indent
= win
.file
.lines
[sel
.line
]:find("^[ \t]*")
485 local line_start
= range
.start
- sel
.col
+ 1
486 local line_begin
= H
.M
.line_begin(win
, range
, pos
)
487 local line_end
= H
.M
.line_end(win
, range
, pos
, 1)
488 return math
.max(line_start
+ indent
, line_begin
), line_end
491 function H
.I
.set_mark_command()
492 local start
= mark
and vis
.win
.file
:mark_get(mark
)
493 local finish
= vis
.win
.selection
.pos
494 local len
= start
and finish
and finish
- start
495 if not len
or len
~= 0 then
496 mark
= vis
.win
.file
:mark_set(finish
)
500 vis
:info("Mark unset")
504 function H
.I
.exchange_point_and_mark()
505 local anchor
= mark
and vis
.win
.file
:mark_get(mark
)
507 mark
= vis
.win
.file
:mark_set(vis
.win
.selection
.pos
)
508 vis
.win
.selection
.pos
= anchor
510 vis
:info("No mark set in this buffer")
514 function H
.I
.mark_sexp()
515 local m
= mark
and vis
.win
.file
:mark_get(mark
)
516 local p
= vis
.win
.selection
.pos
517 local range
= range_by_pos(vis
.win
, m
or p
)
518 local backward
= m
and m
< p
521 newpos
= H
.M
.prev_start(vis
.win
, range
, m
)
523 newpos
= H
.M
.next_finish(vis
.win
, range
, m
, 1)
529 mark
= vis
.win
.file
:mark_set(newpos
)
533 function H
.I
.mark_defun()
534 local m
= mark
and vis
.win
.file
:mark_get(mark
)
535 local p
= vis
.win
.selection
.pos
536 local backward
= m
and m
< p
537 local range
= m
and {start
= math
.min(m
, p
), finish
= math
.max(m
, p
)}
538 or range_by_pos(vis
.win
, p
)
539 local start
, finish
= H
.T
.inner_paragraph(vis
.win
, range
)
540 if start
and finish
then
541 vis
.win
.selection
.pos
= backward
and finish
or start
542 mark
= vis
.win
.file
:mark_set(backward
and start
or finish
)
543 if not m
then vis
:info
"Mark set" end
547 function H
.I
.expand_region()
548 local m
= mark
and vis
.win
.file
:mark_get(mark
)
549 local p
= vis
.win
.selection
.pos
550 local backward
= m
and m
< p
551 local range
= m
and {start
= math
.min(m
, p
), finish
= math
.max(m
, p
)}
552 or range_by_pos(vis
.win
, p
)
553 local start
, finish
= H
.T
.expand_region(vis
.win
, range
)
554 if start
and finish
then
555 vis
.win
.selection
.pos
= backward
and finish
or start
556 mark
= vis
.win
.file
:mark_set(backward
and start
or finish
)
557 if not m
then vis
:info
"Mark set" end
561 function H
.I
.backward_delete()
562 vis
:operator(R
.O
.change
)
563 vis
:motion(VIS_MOVE_CHAR_PREV
)
566 function H
.I
.forward_delete()
567 vis
:operator(R
.O
.change
)
568 vis
:motion(VIS_MOVE_CHAR_NEXT
)
571 -- XXX: force vis into remembering each individual operator call in INSERT mode:
573 local function begin_undo_action(non_destructive
)
574 if insert_mode
and not non_destructive
then
575 vis
.mode
= vis
.modes
.NORMAL
579 local function end_undo_action()
581 vis
.mode
= vis
.modes
.INSERT
585 local function copy_enable(func
)
589 vis
.registers
["0"] = {""}
596 H
.I
.kill_sexp
= copy_enable(function()
597 vis
:operator(R
.O
.change
)
598 vis
:motion(R
.M
.next_finish
)
601 H
.I
.backward_kill_sexp
= copy_enable(function()
602 vis
:operator(R
.O
.change
)
603 vis
:motion(mark_active() and R
.M
.mark
or R
.M
.prev_start
)
606 H
.I
.kill_sentence
= copy_enable(function()
607 vis
:operator(R
.O
.change
)
608 vis
:motion(R
.M
.end_of_sentence
)
611 H
.I
.backward_kill_sentence
= copy_enable(function()
612 vis
:operator(R
.O
.change
)
613 vis
:motion(R
.M
.beginning_of_sentence
)
616 H
.I
.backward_kill_line
= copy_enable(function()
617 vis
:operator(R
.O
.change
)
618 vis
:motion(R
.M
.line_begin
)
621 H
.I
.kill
= copy_enable(function()
622 vis
:operator(R
.O
.change
)
623 vis
:motion(R
.M
.line_end
)
626 H
.I
.forward_kill_word
= copy_enable(function()
627 vis
:operator(R
.O
.change
)
628 vis
:motion(R
.M
.forward_word
)
631 H
.I
.backward_kill_word
= copy_enable(function()
632 vis
:operator(R
.O
.change
)
633 vis
:motion(R
.M
.backward_word
)
636 H
.I
.splice_sexp_killing_forward
= copy_enable(function()
637 vis
:operator(R
.O
.delete
)
638 vis
:motion(R
.M
.forward_up
)
641 H
.I
.splice_sexp_killing_backward
= copy_enable(function()
642 vis
:operator(R
.O
.delete
)
643 vis
:motion(R
.M
.backward_up
)
646 function H
.I
.eval_last_sexp()
647 vis
:operator(R
.O
.eval_defun
)
648 vis
:motion(R
.M
.backward
)
651 local function eval_repl_line(win
, range
, pos
)
652 local bol
= H
.T
.outer_paragraph(win
, range
)
653 return bol
and H
.O
.eval_defun(win
.file
, {start
= bol
, finish
= pos
}, pos
)
656 function H
.O
.format(_
, range
, pos
)
657 local cursor
= range_by_pos(vis
.win
, pos
)
658 -- XXX: don't call range_by_pos() from the argument list, as it returns a second value
659 return env
[vis
.win
].edit
.refmt_at(range
, cursor
) or pos
662 local function make_yank(reg
, sel_number
, handler
)
663 return function(range
)
664 local txt
= vis
.win
.file
:content(range
)
666 local clipboard
= vis
.registers
[reg
]
667 clipboard
[sel_number
] = txt
.. (clipboard
[sel_number
] or "")
668 vis
.registers
[reg
] = clipboard
670 return handler
and handler(range
)
674 local function _delete(range
)
676 return vis
.win
.file
:delete(range
)
679 function H
.O
.delete(_
, range
, pos
)
680 local _
, n
= range_by_pos(vis
.win
, pos
)
681 local delete_and_yank
= make_yank(insert_mode
and do_copy
and "0" or vis
.register
or '"', n
, _delete
)
682 local splicing
= vis
.mode
~= vis
.modes
.VISUAL
and #vis
.win
.selections
== 1
683 return env
[vis
.win
].edit
.delete_splicing(range
, pos
, splicing
, delete_and_yank
)
688 function H
.O
.change(_
, range
, pos
)
689 local _
, n
= range_by_pos(vis
.win
, pos
)
690 local delete_maybe_yank
= insert_mode
and not do_copy
and _delete
or
691 make_yank(insert_mode
and "0" or vis
.register
or '"', n
, _delete
)
692 if selections
== 0 then
693 selections
= #vis
.win
.selections
695 selections
= selections
- 1
696 if selections
== 0 then
697 vis
.mode
= vis
.modes
.INSERT
699 return env
[vis
.win
].edit
.delete_nonsplicing(range
, pos
, delete_maybe_yank
)
702 function H
.O
.yank(_
, range
, pos
)
703 local _
, n
= range_by_pos(vis
.win
, pos
)
704 local yank
= make_yank(insert_mode
and "0" or vis
.register
or '"', n
)
705 local action
= {kill
= false, wrap
= true, splice
= false, func
= yank
}
706 env
[vis
.win
].edit
.pick_out(range
, pos
, action
)
707 return insert_mode
and pos
or range
.start
710 local last_object_yanked
713 local function textobject_includes_separator(object_type
)
714 return (object_type
== "outer_line") or (object_type
== "inner_line")
717 function H
.O
.put_after(file
, range
, pos
)
718 local _
, n
= range_by_pos(vis
.win
, pos
)
719 local clipboard
= vis
.registers
[insert_mode
and 0 or vis
.register
or '"'][n
]
720 if not clipboard
or #clipboard
== 0 then return pos
end
721 local visual
= ({[vis
.modes
.VISUAL
] = true, [vis
.modes
.VISUAL_LINE
] = true})[vis
.mode
]
722 local newpos
= range
.finish
724 -- XXX: like H.O.delete(), but without overwriting the register content
725 local action
= {kill
= true, wrap
= false, splice
= false, func
= _delete
}
726 env
[vis
.win
].edit
.pick_out(range
, pos
, action
)
729 local on_object
= not visual
and last_object_yanked
and (range
.start
< range
.finish
) and pos
< range
.finish
731 newpos
= range
.finish
- (textobject_includes_separator(last_object_yanked
) and 0 or 1)
733 local needs_separator
= not visual
and last_object_yanked
and not textobject_includes_separator(last_object_yanked
)
735 if needs_separator
and on_object
then
736 local sexp
, parent
= env
[vis
.win
].walker
.sexp_at(range
)
737 local nxt
= parent
.is_list
and parent
.after(range
.finish
, startof
, is_comment
)
738 if (nxt
and nxt
.indent
or sexp
and sexp
.indent
and not sexp
.is_line_comment
) then
740 elseif not sexp
.is_line_comment
then
744 newpos
= newpos
+ (needs_separator
and 1 or 0)
745 if last_object_yanked
== "inner_line" then
746 file
:insert(newpos
, "\n")
748 file
:insert(newpos
, ((on_object
and needs_separator
) and separator
or "") .. clipboard
)
749 -- XXX: it is important NOT to "rewind forward", which would leave unparsed gaps,
750 -- leading to incorrect structured actions:
751 if env
[vis
.win
].parser
.tree
.is_parsed(newpos
) then
752 env
[vis
.win
].parser
.tree
.rewind(newpos
)
754 newpos
= newpos
+ (needs_separator
and #separator
or 0) + #clipboard
- (insert_mode
and 0 or 1)
755 local r
= {start
= newpos
, finish
= newpos
+ (insert_mode
and 0 or 1)}
756 local _
, parent
= env
[vis
.win
].walker
.sexp_at(r
, true)
757 return env
[vis
.win
].edit
.refmt_at(parent
, r
) or newpos
760 function H
.O
.put_before(file
, range
, pos
)
761 local _
, n
= range_by_pos(vis
.win
, pos
)
762 local clipboard
= vis
.registers
[insert_mode
and 0 or vis
.register
or '"'][n
]
763 if not clipboard
or #clipboard
== 0 then return pos
end
764 local visual
= ({[vis
.modes
.VISUAL
] = true, [vis
.modes
.VISUAL_LINE
] = true})[vis
.mode
]
766 local action
= {kill
= true, wrap
= false, splice
= false, func
= _delete
}
767 local ndeleted
= env
[vis
.win
].edit
.pick_out(range
, pos
, action
)
768 pos
= pos
- (ndeleted
or 0) + 1
770 local on_object
= not visual
and last_object_yanked
and (range
.start
< range
.finish
)
771 if on_object
and pos
> range
.start
then
774 local needs_separator
= not visual
and last_object_yanked
and not textobject_includes_separator(last_object_yanked
)
776 if needs_separator
and on_object
then
777 local sexp
= env
[vis
.win
].walker
.sexp_at(range
)
778 if (sexp
and sexp
.indent
and not sexp
.is_line_comment
) then
784 if last_object_yanked
== "inner_line" then
785 file
:insert(pos
, "\n")
787 file
:insert(pos
, clipboard
.. ((on_object
and needs_separator
) and separator
or ""))
788 -- XXX: it is important NOT to "rewind forward", which would leave unparsed gaps,
789 -- leading to incorrect structured actions:
790 if env
[vis
.win
].parser
.tree
.is_parsed(pos
) then
791 env
[vis
.win
].parser
.tree
.rewind(pos
)
793 local r
= {start
= pos
, finish
= pos
}
794 local _
, parent
= env
[vis
.win
].walker
.sexp_at(r
, true)
795 return env
[vis
.win
].edit
.refmt_at(parent
, r
) or pos
798 function H
.O
.join_line(_
, range
, pos
)
799 return env
[vis
.win
].edit
.join_line(range
, pos
) or pos
802 function H
.O
.wrap_round(_
, range
, pos
)
803 return env
[vis
.win
].edit
.wrap_round(range
, pos
, M
.auto_square_brackets
)
806 function H
.O
.wrap_square(_
, range
, pos
)
807 return env
[vis
.win
].edit
.wrap_square(range
, pos
, M
.auto_square_brackets
)
810 function H
.O
.wrap_curly(_
, range
, pos
)
811 return env
[vis
.win
].edit
.wrap_curly(range
, pos
, M
.auto_square_brackets
)
814 function H
.O
.wrap_doublequote(_
, range
, pos
)
815 return env
[vis
.win
].edit
.wrap_doublequote(range
, pos
, M
.auto_square_brackets
)
818 function H
.O
.meta_doublequote(_
, range
, pos
)
819 local escaped
= env
[vis
.win
].walker
.escaped_at(range
)
821 return env
[vis
.win
].edit
.wrap_doublequote(range
, pos
, M
.auto_square_brackets
)
822 elseif escaped
.is_string
then
823 return env
[vis
.win
].edit
.close_and_newline(range
, '"')
828 function H
.O
.wrap_comment(_
, range
, pos
)
829 return env
[vis
.win
].edit
.wrap_comment(range
, pos
)
832 function H
.O
.close_and_newline(_
, range
)
833 return env
[vis
.win
].edit
.close_and_newline(range
)
836 function H
.O
.open_next_line(_
, range
)
837 return env
[vis
.win
].edit
.close_and_newline(range
, "\n")
840 function H
.O
.raise_sexp(_
, range
, pos
)
841 return env
[vis
.win
].edit
.raise_sexp(range
, pos
) or pos
844 function H
.O
.splice_sexp(_
, range
, pos
)
845 return env
[vis
.win
].edit
.splice_anylist(range
, pos
) or pos
848 function H
.O
.cycle_wrap(_
, range
, pos
)
849 return env
[vis
.win
].edit
.cycle_wrap(range
, pos
)
852 function H
.O
.forward_slurp(_
, range
)
853 local sexp
= env
[vis
.win
].walker
.sexp_at(range
)
854 if sexp
and sexp
.is_empty
and range
.start
== sexp
.finish
then
855 range
.finish
= sexp
.finish
857 return env
[vis
.win
].edit
.slurp_sexp(range
, true)
860 function H
.O
.backward_slurp(_
, range
)
861 local sexp
= env
[vis
.win
].walker
.sexp_at(range
)
862 local keep_inside
= 0
863 if sexp
and sexp
.is_empty
and range
.start
== sexp
.finish
then
864 range
.finish
= sexp
.finish
867 return env
[vis
.win
].edit
.slurp_sexp(range
) - keep_inside
870 function H
.O
.forward_barf(_
, range
)
871 return env
[vis
.win
].edit
.barf_sexp(range
, true)
874 function H
.O
.backward_barf(_
, range
)
875 return env
[vis
.win
].edit
.barf_sexp(range
)
878 function H
.O
.split_sexp(_
, range
, pos
)
879 return env
[vis
.win
].edit
.split_anylist(range
) or pos
882 function H
.O
.join_sexps(_
, range
, pos
)
883 return env
[vis
.win
].edit
.join_anylists(range
) or pos
886 function H
.O
.convolute_sexp(_
, range
, pos
)
887 return env
[vis
.win
].edit
.convolute_lists(range
) or pos
890 function H
.O
.transpose_sexps(_
, range
, pos
)
891 return env
[vis
.win
].edit
.transpose_sexps(range
) or pos
894 function H
.O
.transpose_words(_
, range
, pos
)
895 return env
[vis
.win
].edit
.transpose_words(range
) or pos
898 function H
.O
.transpose_chars(_
, range
, pos
)
899 return env
[vis
.win
].edit
.transpose_chars(range
) or pos
904 function H
.O
.eval_defun(file
, range
, pos
)
905 if not M
.repl_fifo
or range
.finish
== range
.start
then return pos
end
906 if pos
> range
.finish
then return pos
end
907 local unbalanced
= env
[vis
.win
].parser
.tree
.unbalanced_delimiters(range
)
908 if unbalanced
and #unbalanced
> 0 then return pos
end
910 if not repl_fifo
then
911 repl_fifo
, errmsg
= io
.open(M
.repl_fifo
, "a+")
914 repl_fifo
:write(file
:content(range
), "\n")
922 -- flush any code stuck in the fifo, so starting a REPL after the file has been closed won't read old stuff in.
923 vis
.events
.subscribe(vis
.events
.WIN_CLOSE
, function()
926 repl_fifo
= io
.open(M
.repl_fifo
, "w+")
932 local function textobject_prep(func
, name
)
933 return function(win
, pos
)
934 local start
, finish
= func(win
, range_by_pos(win
, pos
))
935 if about_to_yank
and start
and finish
and finish
> start
then
936 last_object_yanked
= name
942 local function motion_prep(func
)
943 return function(win
, pos
)
944 local exclusivity
= (vis
.mode
== vis
.modes
.OPERATOR_PENDING
or insert_mode
) and 1 or 0
945 local ret
= func(win
, {start
= pos
, finish
= pos
+ (insert_mode
and 0 or 1)}, pos
, exclusivity
)
946 if about_to_yank
and ret
and ret
~= pos
and ({[H
.M
.forward_up
] = true, [H
.M
.backward_up
] = true})[func
] then
947 last_object_yanked
= "outer_list"
953 local function partially_autoselect(handler
)
954 return ({[H
.M
.beginning_of_sentence
] = true, [H
.M
.end_of_sentence
] = true})[handler
]
957 local function prep_map(func
, handler
)
959 local anylist_partially_selected
= false
961 if vis
.mode
== vis
.modes
.INSERT
then
965 if partially_autoselect(handler
) then
966 if vis
.mode
== vis
.modes
.NORMAL
then
967 vis
.mode
= vis
.modes
.VISUAL
968 anylist_partially_selected
= true
970 elseif vis
.mode
~= vis
.modes
.OPERATOR_PENDING
then
971 vis
.mode
= vis
.modes
.NORMAL
975 if ({[H
.M
.prev_section
] = true, [H
.M
.next_section
] = true})[handler
] then
976 vis
:feedkeys("<vis-window-redraw-top>")
977 elseif M
.autoselect
and not anylist_partially_selected
then
978 vis
.mode
= vis
.modes
.VISUAL
979 vis
:textobject(R
.T
.outer_sexp
)
980 if ({[H
.M
.next_start
] = true,
981 [H
.M
.prev_start
] = true,
982 [H
.M
.prev_start_float
] = true,
983 [H
.M
.beginning_of_sentence
] = true,
984 [H
.M
.beginning_of_defun
] = true,
985 [H
.M
.prev_beginning_of_defun
] = true,
986 [H
.M
.backward_up
] = true,
987 [H
.M
.forward_down
] = true})[handler
] then
988 vis
:feedkeys("<vis-selection-flip>")
994 function new
.M(handler
)
995 local id
= vis
:motion_register(motion_prep(handler
))
996 local binding
= id
>= 0 and prep_map(function()
1002 function new
.T(handler
, name
)
1003 local id
= vis
:textobject_register(textobject_prep(handler
, name
))
1004 local binding
= id
>= 0 and prep_map(function()
1010 local function range_for(handler
)
1011 if ({[H
.O
.yank
] = true, [H
.O
.wrap_round
] = true, [H
.O
.wrap_square
] = true, [H
.O
.wrap_curly
] = true,
1012 [H
.O
.wrap_doublequote
] = true, [H
.O
.wrap_comment
] = true, [H
.O
.meta_doublequote
] = true})[handler
]
1013 and mark_active() then
1014 vis
:motion(R
.M
.mark
)
1015 elseif ({[H
.O
.put_after
] = true, [H
.O
.put_before
] = true})[handler
] then
1016 if last_object_yanked
and not insert_mode
then
1017 vis
:textobject(textobject_includes_separator(last_object_yanked
)
1018 and VIS_TEXTOBJECT_OUTER_LINE
1021 vis
:motion(insert_mode
and VIS_MOVE_NOP
or VIS_MOVE_CHAR_NEXT
)
1023 elseif ({[H
.O
.wrap_round
] = true, [H
.O
.wrap_square
] = true, [H
.O
.wrap_curly
] = true,
1024 [H
.O
.wrap_doublequote
] = true, [H
.O
.wrap_comment
] = true, [H
.O
.meta_doublequote
] = true})[handler
] then
1026 vis
:motion(R
.M
.next_finish
)
1028 vis
:textobject(R
.T
.outer_sexp
)
1030 elseif ({[H
.O
.eval_defun
] = true})[handler
] then
1031 vis
:textobject(R
.T
.outer_paragraph
)
1032 elseif ({[H
.O
.format] = true})[handler
] then
1033 vis
:textobject(R
.T
.inner_paragraph
)
1034 elseif ({[H
.O
.splice_sexp
] = true, [H
.O
.raise_sexp
] = true, [H
.O
.cycle_wrap
] = true,
1035 [H
.O
.split_sexp
] = true, [H
.O
.join_sexps
] = true,
1036 [H
.O
.forward_slurp
] = true, [H
.O
.backward_slurp
] = true,
1037 [H
.O
.forward_barf
] = true, [H
.O
.backward_barf
] = true,
1038 [H
.O
.convolute_sexp
] = true,
1039 [H
.O
.close_and_newline
] = true, [H
.O
.open_next_line
] = true, [H
.O
.join_line
] = true,
1040 [H
.O
.transpose_sexps
] = true, [H
.O
.transpose_words
] = true, [H
.O
.transpose_chars
] = true})[handler
] then
1041 vis
:motion(insert_mode
and VIS_MOVE_NOP
or VIS_MOVE_CHAR_NEXT
)
1045 function new
.O(handler
)
1046 local id
= vis
:operator_register(handler
)
1047 local binding
= id
>= 0 and function()
1048 -- TODO: put this if/else block in the operator handlers, as here
1049 -- these statements won't get executed on a dot-repeat.
1050 if ({[H
.O
.yank
] = true, [H
.O
.change
] = true, [H
.O
.delete
] = true})[handler
] then
1051 vis
.registers
[insert_mode
and "0" or vis
.register
or '"'] = {""}
1052 about_to_yank
= true
1053 last_object_yanked
= nil
1055 -- XXX: this will not be reset on built-in and external operators:
1056 about_to_yank
= false
1058 local restore_visual
1059 if M
.autoselect
and vis
.mode
== vis
.modes
.VISUAL
then
1060 restore_visual
= true
1062 if handler
== H
.O
.join_line
and vis
.mode
== vis
.modes
.VISUAL
then
1063 vis
.mode
= vis
.modes
.NORMAL
1065 begin_undo_action(handler
== H
.O
.yank
)
1067 if vis
.mode
== vis
.modes
.OPERATOR_PENDING
then
1071 if restore_visual
and vis
.mode
== vis
.modes
.NORMAL
then
1078 local function depth_of(word
, nodes
, store
)
1079 for i
= #nodes
, 1, -1 do
1080 if nodes
[i
] and nodes
[i
].is_list
and not nodes
[i
].is_empty
and nodes
[i
][1].text
== word
then
1081 table.insert(store
, i
)
1087 local match_module
= {
1088 scheme
= function(win
, pos
)
1089 local indices
, nodes
= env
[win
].walker
.sexp_path(range_by_pos(win
, pos
))
1090 if not (nodes
[1] and nodes
[1].is_list
and not nodes
[1].is_empty
) then return end
1094 if nodes
[1][1].text
== "use-modules" then -- Guile
1095 if #nodes
[1] == 2 then
1097 elseif indices
[2] and indices
[2] < 2 then
1099 elseif indices
[2] then
1100 node
= nodes
[1][indices
[2]]
1102 elseif indices
[2] and indices
[2] > 2 and nodes
[1][1].text
== "define-module" then -- Guile
1103 if nodes
[1][indices
[2] - 1].text
== "#:use-module" then
1104 node
= nodes
[1][indices
[2]]
1105 elseif nodes
[1][indices
[2]]
.text
== "#:use-module" then
1106 node
= nodes
[1][indices
[2] + 1]
1108 elseif depth_of("require", nodes
, depth
) then -- Racket
1111 if #nodes
[d
] == 2 and (not nodes
[d
][2].is_list
or nodes
[d
][2][1].text
== "submod") then
1113 elseif indices
[d
+ 1] and indices
[d
+ 1] < 2 and (not nodes
[d
][2].is_list
or nodes
[d
][2][1].text
== "submod") then
1115 elseif depth_of("submod", nodes
, smdepth
) then
1116 local s
= smdepth
[1]
1119 if node
and node
.is_list
then cut
= node
[1].text
end
1121 return node
and node
.text
1122 :match("%(?".. cut
.."[ \n]*([^)]+)")
1123 :gsub('"', "") -- mostly because "." and ".." in Racket
1125 :gsub("[ \n]+", "/")
1127 fennel
= function(win
, pos
)
1128 local _
, nodes
= env
[win
].walker
.sexp_path(range_by_pos(win
, pos
))
1129 if not (nodes
[1] and nodes
[1].is_list
and not nodes
[1].is_empty
) then return end
1132 if depth_of("require", nodes
, depth
) then
1136 return node
and node
.text
1142 vis
.events
.subscribe(vis
.events
.INIT
, function()
1143 for kind
, register_new
in pairs(new
) do
1144 for name
, handler
in pairs(H
[kind
]) do
1145 R
[kind
][name
], B
[kind
][name
] = register_new(handler
, name
)
1148 table.insert(keycfg
, (require(cwd
..".keys-vim")(B
, H
)))
1150 table.insert(keycfg
, require(cwd
..".keys-vim-compat")(B
, H
))
1153 table.insert(keycfg
, (require(cwd
..".keys-emacs")(B
, H
)))
1155 if vis_goto_file
then
1156 -- vis-goto-file is no good for parsing multiline includeexpr,
1157 -- provide our own helpers:
1158 for syntax
, helper
in pairs(match_module
) do
1159 vis_goto_file(syntax
, helper
)
1164 local function seek(selection
)
1165 return function(offset
)
1166 selection
.pos
= offset
1170 vis
.events
.subscribe(vis
.events
.INPUT
, function(char
)
1171 if not vis_parkour(vis
.win
) then return end
1174 local winput
= env
[win
].input
1175 local consumed_char
= char
:find(env
[win
].consumed_char
)
1177 -- XXX: no undo entry is saved for newlines, due to an implementation detail of begin_undo_action():
1178 -- it switches to NORMAL mode, and vis cleans up the indentation whitespace on the new empty line.
1179 if consumed_char
and char
~= "\n" then begin_undo_action() end
1180 for i
= #win
.selections
, 1, -1 do
1181 local selection
= win
.selections
[i
]
1182 local m
= mark
and win
.file
:mark_get(mark
)
1183 local p
= selection
.pos
1184 local range
= m
and m
~= p
and
1185 {start
= math
.min(m
, p
), finish
= math
.max(m
, p
)} or
1186 {start
= p
, finish
= p
}
1187 if char
== "\n" then
1188 local sexp
, parent
= env
[win
].walker
.sexp_at(range
, true)
1189 if parent
.is_root
and (not sexp
or (not sexp
.is_comment
and sexp
.finish
+ 1 == range
.start
)) then
1190 local same_line
= not not sexp
1192 local line
, pos
= selection
.line
, selection
.pos
1193 -- XXX: the only call site without a skip argument (is_comment)
1194 -- was it intentional?
1195 local prev_finish
= env
[win
].walker
.finish_before(range
)
1197 seek(selection
)(prev_finish
)
1198 same_line
= line
== selection
.line
1199 seek(selection
)(pos
)
1202 if sexp
or same_line
then
1203 eval_repl_line(win
, range
, selection
.pos
)
1207 ret
= winput
.insert(range
, seek(selection
), char
, M
.auto_square_brackets
)
1209 -- XXX: this ridiculous sequence is necessary to save an undo history entry
1210 -- for autopairs insertion _distinct_ from the regular characters afterwards.
1211 if consumed_char
and char
~= "\n" then end_undo_action() begin_undo_action() end_undo_action() end
1217 -- XXX: other plugins can check this to avoid collisions.
1218 -- (in Vis, mapping and operator handlers can't be composed)
1219 function vis_parkour(win
)
1220 return supports
[win
.syntax
]