4 local walker
= require
'parkour.walker'
5 local parser
= require
'parkour.parser'
6 local input
= require
'parkour.input'
7 local edit
= require
'parkour.edit'
11 motion
= {}, textobject
= {}, operator
= {},
12 map
= {}, imap
= {}, raw
= {},
13 supports
= {lisp
= true, scheme
= true, clojure
= true, fennel
= true},
14 keytheme
= {vim
= true, emacs
= true},
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
26 local insert_mode
= 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
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
)
37 win
:map(mode
, k
, action
[mode
].binding
)
44 local H
= {M
= {}, T
= {}, I
= {}, O
= {}, A
={}}
46 local function iprep(func
, win
)
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
))
60 local function wrap_do(action
)
62 local sequence
= "<vis-"..action
..">"
63 if vis
.mode
== vis
.modes
.VISUAL
then
64 sequence
= sequence
.. "<vis-selections-remove-all>"
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)
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
80 vis
.mode
= vis
.modes
.INSERT
83 local function backslash(keys
)
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
91 win
.file
:insert(pos
, bs
)
92 win
.selection
.pos
= pos
+ #bs
93 env
[win
].parser
.tree
.rewind(pos
)
97 if #keys
< 1 then vis
:info("Character to escape: ") return -1 end
99 for i
= #win
.selections
, 1, -1 do
100 local selection
= win
.selections
[i
]
101 local pos
= selection
.pos
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
)
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
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
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
)
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
)
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
)
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
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
)
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
)
214 vis
:command("set expandtab")
215 vis
:command("set tabwidth "..tabwidth
)
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
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
)
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
)
250 newpos
= env
[win
].walker
.forward_word(range
, true)
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
)
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
)
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
)
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
)
407 vis
.registers
['0'] = {""}
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
)
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
)
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
)
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
)
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
)
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
)
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
)
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
)
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
)
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
)
514 r
.finish
= next_sexp
.start
516 local prev_sexp
= parent
.before(r
.start
, finishof
)
517 r
.start
= prev_sexp
and prev_sexp
.finish
+ 1 or r
.start
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
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
544 return pos
- ndeleted
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
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
)
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
]
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
]
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
]
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
607 local needs_separator
= not visual
and not textobject_includes_separator(last_object_yanked
)
608 local clipboard
= vis
.registers
[vis
.register
or '"'][n
]
610 file
:insert(pos
, clipboard
.. ((on_object
and needs_separator
) and " " or ""))
612 env
[vis
.win
].parser
.tree
.rewind(pos
)
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
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
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
690 if not repl_fifo
then
691 repl_fifo
, errmsg
= io
.open(M
.repl_fifo
, "a+")
694 repl_fifo
:write(file
:content(range
), "\n")
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(_
)
706 repl_fifo
= io
.open(M
.repl_fifo
, "w+")
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
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
732 local function prep_map(func
, handler
)
735 if vis
.mode
== vis
.modes
.INSERT
then
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
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,
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
)
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()
787 last_object_yanked
= last_object_type
789 local action
= {binding
= binding
, id
= id
}
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 '"'] = {""}
803 if M
.autoselect
and vis
.mode
== vis
.modes
.VISUAL
then
804 restore_visual
= true
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
)
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
831 local action
= {binding
= binding
, id
= id
}
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
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
)
863 table.insert(M
[mode
][action
], key
)
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
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
}
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
892 local line
, pos
= selection
.line
, selection
.pos
893 local prev_finish
= env
[win
].walker
.finish_before(range
)
895 seek(selection
)(prev_finish
)
896 same_line
= line
== selection
.line
900 if sexp
or same_line
then
901 eval_repl_line(win
, range
, selection
.pos
)
906 ret
= (vis
.mode
== vis
.modes
.REPLACE
and winput
.replace
or winput
.insert
)(range
, seek(selection
), char
)
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
)