add option validation
[vis-parkour.git] / init.lua
blob67e37e608effcb282095d51bd1a59351fe656544
1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
4 local M = {
5 auto_square_brackets = false,
6 lispwords = {},
7 repl_fifo = false,
8 emacs = false,
9 autoselect = false,
12 require("vis")
13 local vis = vis
15 -- XXX: in Lua 5.2 unpack() was moved into table
16 local unpack = table.unpack or unpack
18 local env = {}
19 local keycfg = {}
21 local cwd = ...
22 local init, supports = unpack((require(cwd..".parkour")))
24 local do_copy = false
25 local insert_mode = false
26 local mark
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))
41 end})
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
49 end
50 end)
51 end}
52 t[filetype] = setmetatable({}, recfg)
53 return t[filetype]
54 end
55 end})
57 local function win_map(win, kind, key, action)
58 if kind == "T" then
59 win:map(vis.modes.OPERATOR_PENDING, key, action)
60 if not M.autoselect then
61 win:map(vis.modes.VISUAL, key, action)
62 end
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)
70 end
71 end
73 local function mark_active() return insert_mode and mark end
75 local function iprep(func, win)
76 return function()
77 insert_mode = true
78 func(win)
79 insert_mode = false
80 end
81 end
83 local function imap(win, key, action)
84 win:map(vis.modes.INSERT, key, iprep(action, win))
85 end
87 local function rewind_on(action)
88 return function()
89 local sequence = "<vis-"..action..">"
90 if vis.mode == vis.modes.VISUAL then
91 sequence = sequence .. "<vis-selections-remove-all>"
92 end
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)
97 end
98 end
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)
116 local win = vis.win
117 local sel_is_escaped = {}
118 local range = {}
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
132 local prefix = "\\"
133 win.file:insert(pos, prefix..keys)
134 selection.pos = pos + #(prefix..keys)
135 env[win].parser.tree.rewind(pos)
137 return #keys
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
152 return selection
157 local function autoselect_region()
158 vis.mode = vis.modes.VISUAL
159 vis:textobject(R.T.expand_region)
162 local last_mode
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
169 mark = nil
171 last_mode = vis.mode
172 end)
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)
182 end)
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)
187 mark = nil
188 return win.file:insert(pos, txt)
190 local function delete(pos, len)
191 mark = nil
192 return win.file:delete(pos, len)
194 local function read(base, len)
195 if base and base >= win.file.size then return end
196 base = base or 0
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)
203 if sel then
204 local line_start = sel.pos - sel.col + 1
205 return line_start + #win.file.lines[sel.line]
206 else
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().
212 local horizon = 80
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)
219 if sel then
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
226 if #ch == 1 then
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)
247 if setup["raw"] then
248 setup["raw"](win)
251 win:map(vis.modes.INSERT, "<Enter>", function() vis:feedkeys("\n") end)
252 win:map(vis.modes.INSERT, "\\", backslash)
253 if M.autoselect then
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)
270 end)
271 vis:command("set expandtab")
272 end)
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)
448 if sexp then
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)
457 if sexps then
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)
497 vis:info"Mark set"
498 else
499 mark = nil
500 vis:info("Mark unset")
504 function H.I.exchange_point_and_mark()
505 local anchor = mark and vis.win.file:mark_get(mark)
506 if anchor then
507 mark = vis.win.file:mark_set(vis.win.selection.pos)
508 vis.win.selection.pos = anchor
509 else
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
519 local newpos
520 if backward then
521 newpos = H.M.prev_start(vis.win, range, m)
522 else
523 newpos = H.M.next_finish(vis.win, range, m, 1)
525 if newpos then
526 if not mark then
527 vis:info"Mark set"
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()
580 if insert_mode then
581 vis.mode = vis.modes.INSERT
585 local function copy_enable(func)
586 return function(...)
587 do_copy = true
588 begin_undo_action()
589 vis.registers["0"] = {""}
590 func(...)
591 end_undo_action()
592 do_copy = false
596 H.I.kill_sexp = copy_enable(function()
597 vis:operator(R.O.change)
598 vis:motion(R.M.next_finish)
599 end)
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)
604 end)
606 H.I.kill_sentence = copy_enable(function()
607 vis:operator(R.O.change)
608 vis:motion(R.M.end_of_sentence)
609 end)
611 H.I.backward_kill_sentence = copy_enable(function()
612 vis:operator(R.O.change)
613 vis:motion(R.M.beginning_of_sentence)
614 end)
616 H.I.backward_kill_line = copy_enable(function()
617 vis:operator(R.O.change)
618 vis:motion(R.M.line_begin)
619 end)
621 H.I.kill = copy_enable(function()
622 vis:operator(R.O.change)
623 vis:motion(R.M.line_end)
624 end)
626 H.I.forward_kill_word = copy_enable(function()
627 vis:operator(R.O.change)
628 vis:motion(R.M.forward_word)
629 end)
631 H.I.backward_kill_word = copy_enable(function()
632 vis:operator(R.O.change)
633 vis:motion(R.M.backward_word)
634 end)
636 H.I.splice_sexp_killing_forward = copy_enable(function()
637 vis:operator(R.O.delete)
638 vis:motion(R.M.forward_up)
639 end)
641 H.I.splice_sexp_killing_backward = copy_enable(function()
642 vis:operator(R.O.delete)
643 vis:motion(R.M.backward_up)
644 end)
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)
665 if txt then
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)
675 mark = nil
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)
686 local selections = 0
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
711 local about_to_yank
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
723 if visual then
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)
727 newpos = range.start
729 local on_object = not visual and last_object_yanked and (range.start < range.finish) and pos < range.finish
730 if on_object then
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)
734 local separator = ""
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
739 separator = "\n"
740 elseif not sexp.is_line_comment then
741 separator = " "
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]
765 if visual then
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
772 pos = range.start
774 local needs_separator = not visual and last_object_yanked and not textobject_includes_separator(last_object_yanked)
775 local separator = ""
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
779 separator = "\n"
780 else
781 separator = " "
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)
820 if not escaped then
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, '"')
825 return pos
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
865 keep_inside = 1
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
902 local repl_fifo
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
909 local errmsg
910 if not repl_fifo then
911 repl_fifo, errmsg = io.open(M.repl_fifo, "a+")
913 if repl_fifo then
914 repl_fifo:write(file:content(range), "\n")
915 repl_fifo:flush()
916 elseif errmsg then
917 vis:info(errmsg)
919 return pos
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()
924 if repl_fifo then
925 repl_fifo:close()
926 repl_fifo = io.open(M.repl_fifo, "w+")
927 repl_fifo:close()
928 repl_fifo = nil
930 end)
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
938 return start, finish
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"
949 return ret or pos
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)
958 return function()
959 local anylist_partially_selected = false
960 if M.autoselect then
961 if vis.mode == vis.modes.INSERT then
962 func()
963 return
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
974 func()
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()
997 vis:motion(id)
998 end, handler)
999 return id, binding
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()
1005 vis:textobject(id)
1006 end, handler)
1007 return id, binding
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
1019 or R.T.outer_sexp)
1020 else
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
1025 if insert_mode then
1026 vis:motion(R.M.next_finish)
1027 else
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
1054 else
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)
1066 vis:operator(id)
1067 if vis.mode == vis.modes.OPERATOR_PENDING then
1068 range_for(handler)
1070 end_undo_action()
1071 if restore_visual and vis.mode == vis.modes.NORMAL then
1072 autoselect_region()
1075 return id, binding
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)
1082 return true
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
1091 local node
1092 local cut = ""
1093 local depth = {}
1094 if nodes[1][1].text == "use-modules" then -- Guile
1095 if #nodes[1] == 2 then
1096 node = nodes[1][2]
1097 elseif indices[2] and indices[2] < 2 then
1098 node = nodes[1][2]
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
1109 local d = depth[1]
1110 local smdepth = {}
1111 if #nodes[d] == 2 and (not nodes[d][2].is_list or nodes[d][2][1].text == "submod") then
1112 node = nodes[d][2]
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
1114 node = nodes[d][2]
1115 elseif depth_of("submod", nodes, smdepth) then
1116 local s = smdepth[1]
1117 node = nodes[s]
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
1124 :gsub("'", "")
1125 :gsub("[ \n]+", "/")
1126 end,
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
1130 local node
1131 local depth = {}
1132 if depth_of("require", nodes, depth) then
1133 local d = depth[1]
1134 node = nodes[d][2]
1136 return node and node.text
1137 :match("%:(.+)")
1138 :gsub("%.", "/")
1139 end,
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)))
1149 if M.compat then
1150 table.insert(keycfg, require(cwd..".keys-vim-compat")(B, H))
1152 if M.emacs then
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)
1162 end)
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
1172 local ret
1173 local win = vis.win
1174 local winput = env[win].input
1175 local consumed_char = char:find(env[win].consumed_char)
1176 insert_mode = true
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
1191 if not sexp then
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)
1196 if prev_finish then
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
1212 insert_mode = false
1213 mark = nil
1214 return ret
1215 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]
1223 return M