vis: fix a rewind assert when pasting
[lisp-parkour.git] / vis / init.lua
blob644488fdc8512b586d6895b1564bee4c736eacbb
1 require'vis'
2 local vis = vis
4 local walker = require'parkour.walker'
5 local parser = require'parkour.parser'
6 local input = require'parkour.input'
7 local edit = require'parkour.edit'
8 local env = {}
10 local M = {
11 motion = {}, textobject = {}, operator = {},
12 map = {}, imap = {}, raw = {},
13 supports = {lisp = true, scheme = true, clojure = true, fennel = true},
14 keytheme = {vim = true, emacs = true},
15 autoselect = false,
16 repl_fifo = nil,
19 -- IDs of built-in motions and textobjects used in the plugin (copied from vis.h)
20 local VIS_MOVE_CHAR_PREV = 16
21 local VIS_MOVE_CHAR_NEXT = 17
22 local VIS_MOVE_NOP = 60
23 local VIS_TEXTOBJECT_OUTER_LINE = 23
25 local tabwidth = 2
26 local insert_mode = false
27 local do_copy = false
29 local function win_map(name, win, key, action)
30 if not (key and action) then return end
31 for _, mode in pairs(vis.modes) do
32 if action[mode] then
33 for _, k in pairs(type(key) == "table" and key or {key}) do
34 if M.textobject[name] and M.autoselect then
35 vis:unmap(vis.modes.VISUAL, k)
36 elseif k then
37 win:map(mode, k, action[mode].binding)
38 end
39 end
40 end
41 end
42 end
44 local H = {M = {}, T = {}, I = {}, O = {}, A ={}}
46 local function iprep(func, win)
47 return function()
48 insert_mode = true
49 func(win)
50 insert_mode = false
51 end
52 end
54 local function ins_map(win, key, action)
55 for _, k in pairs(type(key) == "table" and key or {key}) do
56 win:map(vis.modes.INSERT, k, iprep(action, win))
57 end
58 end
60 local function wrap_do(action)
61 return function()
62 local sequence = "<vis-"..action..">"
63 if vis.mode == vis.modes.VISUAL then
64 sequence = sequence .. "<vis-selections-remove-all>"
65 end
66 vis:feedkeys(sequence)
67 -- force a reparse from the very beginning, as undo/redo can create or remove
68 -- multiple paragraphs in one go and I don't know how much to rewind.
69 env[vis.win].parser.tree.rewind(0)
70 end
71 end
73 H.A.undo = wrap_do("undo")
74 H.A.redo = wrap_do("redo")
76 local function vinsert(win, append)
77 for selection in win:selections_iterator() do
78 selection.pos = append and selection.range.finish or selection.range.start
79 end
80 vis.mode = vis.modes.INSERT
81 end
83 local function backslash(keys)
84 local win = vis.win
85 if #win.selections == 1 then
86 -- XXX: comments only work properly with a single selection
87 local escaped = env[win].walker.escaped(win.selection.range)
88 if escaped and escaped.is_comment then
89 local pos = win.selection.pos
90 local bs = "\\"
91 win.file:insert(pos, bs)
92 win.selection.pos = pos + #bs
93 env[win].parser.tree.rewind(pos)
94 return
95 end
96 end
97 if #keys < 1 then vis:info("Character to escape: ") return -1 end
98 if #keys == 1 then
99 for i = #win.selections, 1, -1 do
100 local selection = win.selections[i]
101 local pos = selection.pos
102 local prefix = "\\"
103 local escaped = env[win].walker.escaped(selection.range)
104 if pos == 0 or not escaped then
105 prefix = "#" .. prefix
107 win.file:insert(pos, prefix..keys)
108 selection.pos = pos + #(prefix..keys)
109 env[win].parser.tree.rewind(pos)
112 return #keys
115 local function range_by_pos(win, pos)
116 for selection in win:selections_iterator() do
117 if selection.pos == pos then
118 return insert_mode and {start = pos, finish = pos} or selection.range, selection.number
121 return {start = pos, finish = pos + (insert_mode and 0 or 1)}
124 local function selection_by_pos(win, pos)
125 for selection in win:selections_iterator() do
126 if selection.pos == pos or selection.range.start == pos then
127 return selection
132 vis.events.subscribe(vis.events.WIN_OPEN, function(win)
133 if vis_parkour(win) then
134 local function insert(pos, char)
135 return win.file:insert(pos, char)
137 local function delete(pos, len)
138 return win.file:delete(pos, len)
140 local function read(base, len)
141 if base == win.file.size - 1 then return end
142 base = base or 0
143 len = len or win.file.size
144 local more = base + len < win.file.size
145 return win.file:content(base, len), more
147 -- TODO: this can be implemented in the core modules, without using editor-specific APIs:
148 local function eol_at(pos)
149 local sel = selection_by_pos(win, pos)
150 if sel then
151 local line_start = sel.pos - sel.col
152 return line_start + #win.file.lines[sel.line]
155 -- TODO: this can be implemented in the core modules, without using editor-specific APIs:
156 local function bol_at(pos)
157 local sel = selection_by_pos(win, pos)
158 if sel then
159 return sel.pos - sel.col + 1
162 local p, d1, D2 = parser.new(win.syntax, read)
163 local w = walker.new(p, eol_at, bol_at)
164 local e = edit.new(p, w, insert, delete)
165 local i = input.new(p, d1, D2, w, e, insert, delete, read, eol_at)
166 env[win] = {
167 parser = p,
168 walker = w,
169 input = i,
170 edit = e,
172 for name, mapping in pairs(M.map) do
173 win_map(name, win, mapping, M.motion[name] or M.textobject[name] or M.operator[name])
175 for name, mapping in pairs(M.imap) do
176 ins_map(win, mapping, H.I[name] or
177 H.M[name] and M.motion[name][vis.modes.OPERATOR_PENDING].binding or
178 H.O[name] and M.operator[name][vis.modes.NORMAL].binding)
180 for _, bindings in pairs(M.raw) do
181 bindings(win)
183 for name, handler in pairs(H.A) do
184 for _, k in pairs(type(M.map[name]) == "table" and M.map[name] or {M.map[name]}) do
185 win:map(vis.modes.NORMAL, k, handler)
186 win:map(vis.modes.VISUAL, k, handler)
188 for _, k in pairs(type(M.imap[name]) == "table" and M.imap[name] or {M.imap[name]}) do
189 win:map(vis.modes.INSERT, k, handler)
192 vis:unmap(vis.modes.INSERT, '<Enter>')
193 win:unmap(vis.modes.INSERT, '<Enter>')
194 vis:map(vis.modes.INSERT, '<Enter>', function() vis:feedkeys("\n") end)
195 vis:map(vis.modes.INSERT, '\\', backslash)
196 if M.autoselect then
197 win:unmap(vis.modes.VISUAL, "i")
198 win:unmap(vis.modes.VISUAL, "a")
199 -- make sure there are no mappings that start with i or a,
200 -- otherwise switching from visual straight to insert mode won't work:
201 local bindings = vis:mappings(vis.modes.VISUAL)
202 for key in pairs(bindings) do
203 if key:sub(1, 1):match("[ia]") then
204 vis:unmap(vis.modes.VISUAL, key)
207 win:map(vis.modes.VISUAL, "i", function() vinsert(win) end)
208 win:map(vis.modes.VISUAL, "a", function() vinsert(win, true) end)
209 win:map(vis.modes.NORMAL, "v", function()
210 vis.mode = vis.modes.VISUAL
211 vis:textobject(M.textobject.outer_sexp[vis.modes.VISUAL].id)
212 end)
214 vis:command("set expandtab")
215 vis:command("set tabwidth "..tabwidth)
217 end)
219 vis.events.subscribe(vis.events.START, function()
220 if vis_parkour(vis.win) and M.keytheme.emacs and not M.keytheme.vim then
221 vis.mode = vis.modes.INSERT
223 end)
225 local function is_comment(node) return node.is_comment end
227 function H.M.prev_start(win, range, pos)
228 local escaped = env[win].walker.escaped(range)
229 if escaped then
230 return env[win].walker.backward_word(range, true) or pos
232 return env[win].walker.start_before(range, is_comment) or pos
235 function H.M.next_start(win, range, pos)
236 -- TODO: make it work in escaped regions
237 return env[win].walker.start_after(range, is_comment) or pos
240 function H.M.prev_finish(win, range, pos, exclusive)
241 -- TODO: make it work in escaped regions
242 local newpos = env[win].walker.finish_before(range, is_comment)
243 return newpos and newpos - 1 + exclusive or pos
246 function H.M.next_finish(win, range, pos, exclusive)
247 local escaped = env[win].walker.escaped(range)
248 local newpos
249 if escaped then
250 newpos = env[win].walker.forward_word(range, true)
251 else
252 newpos = env[win].walker.finish_after(range, is_comment)
254 return newpos and newpos - 1 + exclusive or pos
257 function H.M.forward(win, range, pos)
258 return env[win].walker.finish_after(range, is_comment) or pos
261 function H.M.backward(win, range, pos)
262 return env[win].walker.start_before(range, is_comment) or pos
265 function H.M.forward_up(win, range, pos, exclusive)
266 -- XXX: make splice-killing-forward work even on the closing paren:
267 if exclusive > 0 then
268 local sexp = env[win].walker.sexp_at(range)
269 if sexp and sexp.finish == pos then
270 range.finish = range.finish - exclusive
273 local newpos = env[win].walker.finish_up_after(range)
274 return newpos and newpos - 1 + exclusive or pos
277 function H.M.forward_down(win, range, pos)
278 local newpos = env[win].walker.start_down_after(range, is_comment)
279 return newpos or pos
282 function H.M.backward_up(win, range, pos)
283 return env[win].walker.start_up_before(range) or pos
286 function H.M.backward_down(win, range, pos, exclusive)
287 local newpos, list = env[win].walker.finish_down_before(range, is_comment)
288 return newpos and (list.is_empty and newpos or newpos - 1 + exclusive) or pos
291 function H.M.next_section(win, range, pos)
292 local newpos = env[win].walker.find_after(range, function(t) return t.section and t.start > range.start end)
293 return newpos and newpos.start or pos
296 function H.M.prev_section(win, range, _)
297 local newpos = env[win].walker.find_before(range, function(t) return t.section end)
298 return newpos and newpos.start or 0
301 local function startof(node) return node.start end
302 local function finishof(node) return node.finish end
304 function H.M.beginning_of_defun(win, range, pos)
305 return env[win].walker.beginning_of_defun(range, not (insert_mode or M.autoselect)) or pos
308 function H.M.end_of_defun(win, range, pos)
309 local next_line = insert_mode or not M.autoselect
310 local newpos = env[win].walker.end_of_defun(range, next_line)
311 return newpos and newpos - (M.autoselect and not insert_mode and 1 or 0) or pos
314 function H.M.line_begin(win, range, _)
315 return env[win].walker.prev_start_wrapped(range)
318 function H.M.line_end(win, range, _, exclusive)
319 return env[win].walker.next_finish_wrapped(range) - 1 + exclusive
322 function H.M.backward_word(win, range, pos)
323 return env[win].walker.backward_word(range, true) or pos
326 function H.M.forward_word(win, range, pos, exclusive)
327 local newpos = env[win].walker.forward_word(range, true)
328 return newpos and newpos - 1 + exclusive or pos
331 function H.T.outer_list(win, range)
332 local _, parent = env[win].walker.sexp_at(range, true)
333 if parent and parent.is_list then
334 return parent.start, parent.finish + 1
338 function H.T.inner_list(win, range)
339 local _, parent = env[win].walker.sexp_at(range, true)
340 if parent and parent.is_list then
341 return parent.start + (parent.p and #parent.p or 0) + #parent.d, parent.finish
345 function H.T.outer_sexp(win, range)
346 local sexp = env[win].walker.sexp_at(range)
347 if sexp then
348 return sexp.start, sexp.finish + 1
352 H.T.inner_sexp = H.T.outer_sexp
354 function H.T.outer_paragraph(win, range)
355 local sexp = env[win].walker.paragraph_at(range)
356 if sexp then
357 return sexp.start, sexp.finish + 1
361 H.T.inner_paragraph = H.T.outer_paragraph
363 function H.T.outer_string(win, range)
364 local sexp = env[win].walker.escaped(range)
365 if sexp and sexp.is_string then
366 return sexp.start, sexp.finish + #sexp.d
370 function H.T.outer_line(win, range)
371 local sel = selection_by_pos(win, range.start)
372 if not sel then return end
373 local line_start = range.start - sel.col + 1
374 local line_finish = line_start + #win.file.lines[sel.line]
375 return line_start, line_finish + 1
378 function H.T.inner_line(win, range)
379 local sel = selection_by_pos(win, range.start)
380 if not sel then return end
381 local _, indent = win.file.lines[sel.line]:find("^[ \t]*")
382 local line_start = range.start - sel.col + 1
383 local line_finish = line_start + #win.file.lines[sel.line]
384 return line_start + indent, line_finish
387 function H.I.backward_delete(win)
388 local line = win.file.lines[win.selection.line]
389 local col = win.selection.col - 1
390 if line:sub(1, col):match("^[ \t]+$") then
391 -- `smarttab` delete:
392 local remainder = col % tabwidth
393 vis.count = remainder == 0 and tabwidth or remainder
395 vis:operator(M.operator.change[vis.modes.NORMAL].id)
396 vis:motion(VIS_MOVE_CHAR_PREV)
399 function H.I.forward_delete(_)
400 vis:operator(M.operator.change[vis.modes.NORMAL].id)
401 vis:motion(VIS_MOVE_CHAR_NEXT)
404 local function copy_enable(func)
405 return function()
406 do_copy = true
407 vis.registers['0'] = {""}
408 func()
409 do_copy = false
413 H.I.kill_sexp = copy_enable(function()
414 vis:operator(M.operator.change[vis.modes.NORMAL].id)
415 vis:motion(M.motion.next_finish[vis.modes.OPERATOR_PENDING].id)
416 end)
418 H.I.backward_kill_sexp = copy_enable(function()
419 vis:operator(M.operator.change[vis.modes.NORMAL].id)
420 vis:motion(M.motion.prev_start[vis.modes.OPERATOR_PENDING].id)
421 end)
423 H.I.backward_kill_line = copy_enable(function()
424 vis:operator(M.operator.change[vis.modes.NORMAL].id)
425 vis:motion(M.motion.line_begin[vis.modes.OPERATOR_PENDING].id)
426 end)
428 H.I.kill = copy_enable(function()
429 vis:operator(M.operator.change[vis.modes.NORMAL].id)
430 vis:motion(M.motion.line_end[vis.modes.OPERATOR_PENDING].id)
431 end)
433 H.I.forward_kill_word = copy_enable(function()
434 vis:operator(M.operator.change[vis.modes.NORMAL].id)
435 vis:motion(M.motion.forward_word[vis.modes.OPERATOR_PENDING].id)
436 end)
438 H.I.backward_kill_word = copy_enable(function()
439 vis:operator(M.operator.change[vis.modes.NORMAL].id)
440 vis:motion(M.motion.backward_word[vis.modes.OPERATOR_PENDING].id)
441 end)
443 H.I.splice_sexp_killing_forward = copy_enable(function()
444 vis:operator(M.operator.delete[vis.modes.NORMAL].id)
445 vis:motion(M.motion.forward_up[vis.modes.OPERATOR_PENDING].id)
446 end)
448 H.I.splice_sexp_killing_backward = copy_enable(function()
449 vis:operator(M.operator.delete[vis.modes.NORMAL].id)
450 vis:motion(M.motion.backward_up[vis.modes.OPERATOR_PENDING].id)
451 end)
453 function H.I.paste(win)
454 env[win].parser.tree.rewind(win.selections[1].pos)
455 vis:feedkeys('<vis-insert-register>0')
458 function H.I.reindent_defun(_)
459 vis:operator(M.operator.format[vis.modes.NORMAL].id)
460 vis:textobject(M.textobject.inner_paragraph[vis.modes.OPERATOR_PENDING].id)
463 function H.I.eval_last_sexp(_)
464 vis:operator(M.operator.eval_defun[vis.modes.NORMAL].id)
465 vis:motion(M.motion.backward[vis.modes.OPERATOR_PENDING].id)
468 local function eval_repl_line(win, range, pos)
469 local bol = env[win].walker.repl_line_begin(range)
470 return bol and H.O.eval_defun(win.file, {start = bol, finish = pos}, pos)
473 function H.I.end_of_defun(_)
474 vis:motion(M.motion.end_of_defun[vis.modes.NORMAL].id)
477 function H.O.format(_, range, pos)
478 local cursor = range_by_pos(vis.win, pos)
479 -- XXX: don't call range_by_pos() from the argument list, as it returns a second value
480 return env[vis.win].edit.reindent_at(range, cursor) or pos
483 local function make_yank(reg, sel_number, handler)
484 return function(range)
485 local new = vis.win.file:content(range)
486 if new then
487 local old = vis.registers[reg]
488 old[sel_number] = new .. (old[sel_number] or "")
489 vis.registers[reg] = old
491 return handler and handler(range)
495 local function _delete(range)
496 return vis.win.file:delete(range)
499 function H.O.delete(_, range, pos)
500 local _, n = range_by_pos(vis.win, pos)
501 local delete = make_yank(insert_mode and do_copy and '0' or vis.register or '"', n, _delete)
502 local splicing = vis.mode ~= vis.modes.VISUAL and #vis.win.selections == 1
503 local action = {kill = true, wrap = splicing, splice = splicing, func = delete}
504 local sexp = env[vis.win].walker.sexp_at(range, true)
505 local extend = sexp and (sexp.start == range.start and sexp.finish == range.finish - 1 or
506 sexp.is_empty and pos == sexp.finish)
507 local ndeleted, spliced, opening = env[vis.win].edit.pick_out(range, pos, action)
508 local backwards = pos == range.finish
509 if extend or spliced then
510 local r = {start = sexp.start, finish = sexp.start}
511 local _, parent = env[vis.win].walker.sexp_at(r)
512 local next_sexp = parent.after(r.finish, startof)
513 if next_sexp then
514 r.finish = next_sexp.start
515 else
516 local prev_sexp = parent.before(r.start, finishof)
517 r.start = prev_sexp and prev_sexp.finish + 1 or r.start
519 _delete(r)
520 env[vis.win].parser.tree.rewind(r.start)
521 return backwards and r.start or spliced and pos - #opening or r.start
523 return not backwards and ndeleted == 0 and range.finish or range.start
526 local selections = 0
528 function H.O.change(file, range, pos)
529 local _, n = range_by_pos(vis.win, pos)
530 local delete = insert_mode and not do_copy and _delete or
531 make_yank(insert_mode and '0' or vis.register or '"', n, _delete)
532 local action = {kill = true, wrap = false, splice = false, func = delete}
533 local ndeleted, spliced, opening = env[vis.win].edit.pick_out(range, pos, action)
534 if selections == 0 then
535 selections = #vis.win.selections
537 selections = selections - 1
538 if selections == 0 then
539 vis.mode = vis.modes.INSERT
541 local backwards = pos == range.finish
542 if opening then
543 if spliced then
544 return pos - ndeleted
545 else
546 if ndeleted == 0 then
547 local sexp = env[vis.win].walker.sexp_at(range)
548 if sexp and sexp.d and sexp.d:match("^;") then
549 return H.O.join_sexps(file, range, pos)
552 return backwards and (range.finish - ndeleted) or pos
554 else
555 return backwards and range.start or (range.finish - ndeleted)
559 function H.O.yank(_, range, pos)
560 local _, n = range_by_pos(vis.win, pos)
561 local yank = make_yank(vis.register or '"', n)
562 local action = {kill = false, wrap = true, splice = false, func = yank}
563 env[vis.win].edit.pick_out(range, pos, action)
564 return range.start
567 local last_object_yanked
568 local last_object_type
570 local function textobject_includes_separator(object_type)
571 return ({[H.T.outer_line] = true})[object_type]
574 function H.O.put_after(file, range, pos)
575 local _, n = range_by_pos(vis.win, pos)
576 local visual = ({[vis.modes.VISUAL] = true, [vis.modes.VISUAL_LINE] = true})[vis.mode]
577 if visual then
578 -- XXX: like H.O.delete(), but without overwriting the register content
579 local action = {kill = true, wrap = false, splice = false, func = _delete}
580 env[vis.win].edit.pick_out(range, pos, action)
582 local on_object = not visual and (range.start < range.finish)
583 if on_object and pos + 1 < range.finish then
584 pos = range.finish - (textobject_includes_separator(last_object_yanked) and 0 or 1)
586 local needs_separator = not visual and not textobject_includes_separator(last_object_yanked)
587 pos = pos + (needs_separator and 1 or 0)
588 local clipboard = vis.registers[vis.register or '"'][n]
589 if clipboard then
590 file:insert(pos, ((on_object and needs_separator) and " " or "") .. clipboard)
592 env[vis.win].parser.tree.rewind(pos)
593 return range.finish + (needs_separator and 1 or 0)
596 function H.O.put_before(file, range, pos)
597 local _, n = range_by_pos(vis.win, pos)
598 local visual = ({[vis.modes.VISUAL] = true, [vis.modes.VISUAL_LINE] = true})[vis.mode]
599 if visual then
600 local action = {kill = true, wrap = false, splice = false, func = _delete}
601 env[vis.win].edit.pick_out(range, pos, action)
603 local on_object = not visual and (range.start < range.finish)
604 if on_object and pos > range.start then
605 pos = range.start
607 local needs_separator = not visual and not textobject_includes_separator(last_object_yanked)
608 local clipboard = vis.registers[vis.register or '"'][n]
609 if clipboard then
610 file:insert(pos, clipboard.. ((on_object and needs_separator) and " " or ""))
612 env[vis.win].parser.tree.rewind(pos)
613 return range.start
616 function H.O.wrap_round(_, range, pos)
617 return env[vis.win].edit.wrap_round(range, pos)
620 function H.O.meta_doublequote(_, range, pos)
621 return env[vis.win].edit.meta_doublequote(range, pos)
624 function H.O.raise_sexp(_, range, pos)
625 return env[vis.win].edit.raise_sexp(range, pos) or pos
628 function H.O.splice_sexp(_, range, pos)
629 return env[vis.win].edit.splice_sexp(range, pos) or pos
632 function H.O.cycle_wrap(_, range,pos)
633 return env[vis.win].edit.cycle_wrap(range, pos)
636 function H.O.forward_slurp(_, range, _)
637 local sexp = env[vis.win].walker.sexp_at(range)
638 if sexp and sexp.is_empty then
639 range.start = sexp.finish
640 range.finish = sexp.finish
642 return env[vis.win].edit.slurp_sexp(range, true)
645 function H.O.backward_slurp(_, range, _)
646 local sexp = env[vis.win].walker.sexp_at(range)
647 local keep_inside = 0
648 if sexp and sexp.is_empty then
649 range.start = sexp.finish
650 range.finish = sexp.finish
651 keep_inside = 1
653 return env[vis.win].edit.slurp_sexp(range) - keep_inside
656 function H.O.forward_barf(_, range, _)
657 return env[vis.win].edit.barf_sexp(range, true)
660 function H.O.backward_barf(_, range, _)
661 return env[vis.win].edit.barf_sexp(range)
664 function H.O.split_sexp(_, range, pos)
665 return env[vis.win].edit.split_sexp(range) or pos
668 function H.O.join_sexps(_, range, pos)
669 return env[vis.win].edit.join_sexps(range) or pos
672 function H.O.transpose_sexps(_, range, pos)
673 return env[vis.win].edit.transpose_sexps(range) or pos
676 function H.O.transpose_words(_, range, pos)
677 return env[vis.win].edit.transpose_words(range) or pos
680 function H.O.transpose_chars(_, range, pos)
681 return env[vis.win].edit.transpose_chars(range) or pos
684 local repl_fifo
686 function H.O.eval_defun(file, range, pos)
687 if not M.repl_fifo or range.finish == range.start then return pos end
688 if pos > range.finish then return pos end
689 local errmsg
690 if not repl_fifo then
691 repl_fifo, errmsg = io.open(M.repl_fifo, "a+")
693 if repl_fifo then
694 repl_fifo:write(file:content(range), "\n")
695 repl_fifo:flush()
696 elseif errmsg then
697 vis:info(errmsg)
699 return pos
702 -- flush any code stuck in the fifo, so starting a REPL after the file has been closed won't read old stuff in.
703 vis.events.subscribe(vis.events.WIN_CLOSE, function(_)
704 if repl_fifo then
705 repl_fifo:close()
706 repl_fifo = io.open(M.repl_fifo, "w+")
707 repl_fifo:close()
708 repl_fifo = nil
710 end)
712 local function textobject_prep(func)
713 return function(win, pos)
714 local start, finish = func(win, range_by_pos(win, pos))
715 if start and finish then
716 last_object_type = func
718 return start, finish
722 local function motion_prep(func, exclusivity)
723 return function(win, pos)
724 local start = func(win, range_by_pos(win, pos), pos, exclusivity)
725 if (vis.mode == vis.modes.OPERATOR_PENDING or vis.mode == vis.modes.VISUAL) then
726 last_object_type = nil
728 return start
732 local function prep_map(func, handler)
733 return function()
734 if M.autoselect then
735 if vis.mode == vis.modes.INSERT then
736 func()
737 return
739 if ({[H.M.prev_start] = true, [H.M.beginning_of_defun] = true})[handler] and
740 vis.win.selection.pos > vis.win.selection.range.start then
741 vis:feedkeys('<vis-selection-flip>')
743 vis.mode = vis.modes.NORMAL
745 func()
746 if ({[H.M.prev_section] = true, [H.M.next_section] = true})[handler] then
747 vis:feedkeys("<vis-window-redraw-top>")
748 elseif M.autoselect then
749 vis.mode = vis.modes.VISUAL
750 vis:textobject(M.textobject.outer_sexp[vis.modes.VISUAL].id)
755 local function new_motion(handler)
756 local exclusivity = ({
757 [H.M.prev_finish] = 1,
758 [H.M.next_finish] = 1,
759 [H.M.forward_up] = 1,
760 [H.M.backward_down] = 1,
761 [H.M.forward_word] = 1,
762 [H.M.line_end] = 1,
763 })[handler] or 0
765 -- XXX: for motions with no special exclusivity rules this table will "fold" into one element:
766 local variants = {[0] = {0}, [exclusivity] = {exclusivity}}
767 -- registering separate motions that will be bound to the same key was only necessary because
768 -- some motions are inclusive/exclusive depending on the mode, and vis doesn't restore vis.mode when dot-repeating.
769 -- I wanted them to keep their exclusivity rules, even when being dot-repeated.
770 for i, action in pairs(variants) do
771 action.id = vis:motion_register(motion_prep(handler, i))
772 action.binding = action.id >= 0 and prep_map(function()
773 vis:motion(action.id)
774 end, handler)
776 return {
777 [vis.modes.NORMAL] = variants[0],
778 [vis.modes.VISUAL] = variants[0],
779 [vis.modes.OPERATOR_PENDING] = variants[exclusivity],
783 local function new_textobject(handler)
784 local id = vis:textobject_register(textobject_prep(handler))
785 local binding = id >= 0 and function()
786 vis:textobject(id)
787 last_object_yanked = last_object_type
789 local action = {binding = binding, id = id}
790 return {
791 [vis.modes.VISUAL] = action,
792 [vis.modes.OPERATOR_PENDING] = action,
796 local function new_operator(handler)
797 local id = vis:operator_register(handler)
798 local binding = id >= 0 and function()
799 if ({[H.O.yank] = true, [H.O.change] = true, [H.O.delete] = true})[handler] then
800 vis.registers[vis.register or '"'] = {""}
802 local restore_visual
803 if M.autoselect and vis.mode == vis.modes.VISUAL then
804 restore_visual = true
806 vis:operator(id)
807 if vis.mode == vis.modes.OPERATOR_PENDING then
808 if ({[H.O.put_after] = true, [H.O.put_before] = true})[handler] then
809 if last_object_yanked then
810 vis:textobject((last_object_yanked == H.T.outer_line) and VIS_TEXTOBJECT_OUTER_LINE
811 or M.textobject.outer_sexp[vis.modes.OPERATOR_PENDING].id)
812 else
813 vis:motion(VIS_MOVE_NOP)
815 elseif ({[H.O.wrap_round] = true, [H.O.meta_doublequote] = true})[handler] then
816 vis:textobject(M.textobject.outer_sexp[vis.modes.OPERATOR_PENDING].id)
817 elseif ({[H.O.eval_defun] = true})[handler] then
818 vis:textobject(M.textobject.outer_paragraph[vis.modes.OPERATOR_PENDING].id)
819 elseif ({[H.O.splice_sexp] = true, [H.O.raise_sexp] = true, [H.O.cycle_wrap] = true,
820 [H.O.split_sexp] = true, [H.O.join_sexps] = true,
821 [H.O.forward_slurp] = true, [H.O.backward_slurp] = true,
822 [H.O.forward_barf] = true, [H.O.backward_barf] = true,
823 [H.O.transpose_sexps] = true, [H.O.transpose_words] = true, [H.O.transpose_chars] = true})[handler] then
824 vis:motion(insert_mode and VIS_MOVE_NOP or VIS_MOVE_CHAR_NEXT)
827 if restore_visual then
828 vis:feedkeys('v')
831 local action = {binding = binding, id = id}
832 return {
833 [vis.modes.NORMAL] = action,
834 [vis.modes.VISUAL] = action,
838 vis.events.subscribe(vis.events.INIT, function()
839 for name, handler in pairs(H.M) do
840 M.motion[name] = new_motion(handler)
842 for name, handler in pairs(H.T) do
843 M.textobject[name] = new_textobject(handler)
845 for name, handler in pairs(H.O) do
846 M.operator[name] = new_operator(handler)
848 for _, keytheme in pairs(type(M.keytheme) == "table" and M.keytheme or {}) do
849 for mode, bindings in pairs(type(keytheme) == "table" and keytheme or {}) do
850 if M[mode] then
851 if type(bindings) == "function" then
852 table.insert(M[mode], bindings)
853 elseif type(bindings) == "table" then
854 for action, key in pairs(bindings) do
855 if type(M[mode][action]) ~= "table" then
856 M[mode][action] = {M[mode][action]}
858 if type(key) == "table" then
859 for _, k in ipairs(key) do
860 table.insert(M[mode][action], k)
862 else
863 table.insert(M[mode][action], key)
870 end)
872 local function seek(selection)
873 return function(offset)
874 selection.pos = offset
878 vis.events.subscribe(vis.events.INPUT, function(char)
879 if not vis_parkour(vis.win) then return end
880 local ret
881 local win = vis.win
882 local winput = env[win].input
883 for i = #win.selections, 1, -1 do
884 local selection = win.selections[i]
885 local range = {start = selection.pos, finish = selection.pos}
886 if char == "\n" then
887 insert_mode = true
888 local sexp, parent = env[win].walker.sexp_at(range, true)
889 if parent.is_root and (not sexp or (not sexp.is_comment and sexp.finish + 1 == range.start)) then
890 local same_line = not not sexp
891 if not sexp then
892 local line, pos = selection.line, selection.pos
893 local prev_finish = env[win].walker.finish_before(range)
894 if prev_finish then
895 seek(selection)(prev_finish)
896 same_line = line == selection.line
897 seek(selection)(pos)
900 if sexp or same_line then
901 eval_repl_line(win, range, selection.pos)
904 insert_mode = false
906 ret = (vis.mode == vis.modes.REPLACE and winput.replace or winput.insert)(range, seek(selection), char)
908 return ret
909 end)
911 -- XXX: other plugins can use this to check if parkour is handling a window
912 -- and avoid collisions. (in Vis, mapping and operator handlers can't be composed)
913 function vis_parkour(win)
914 return M.supports[win.syntax]
917 for name in pairs(M.keytheme) do
918 M.keytheme[name] = require('parkour.vis.keytheme.'..name)
921 return M