add close_and_newline
[lisp-parkour.git] / edit.lua
blob2aa23c8ad0f554184af8d1b56237630aea1cf879
1 local M = {}
3 -- XXX: in Lua 5.2 unpack() was moved into table
4 local unpack = unpack or table.unpack
6 local function startof(node) return node.start end
7 local function finishof(node) return node.finish end
8 local function is_comment(node) return node.is_comment end
10 function M.new(parser, walker, fmt, write, delete)
12 local function normalize_spacing(start, delta, list)
13 if delta > 0 then
14 table.insert(list, {start + delta, -delta})
15 elseif delta < 0 then
16 table.insert(list, {start, -delta})
17 end
18 return delta
19 end
21 local function refmt_list(parent, base_indent, padj, deltas, keep_electric_space)
22 local adj = 0
23 local pstart = parent.start + (parent.p and #parent.p or 0) + #parent.d
24 if parent.is_empty then
25 normalize_spacing(pstart, parent.finish - pstart, deltas)
26 return deltas
27 end
28 for i, s in ipairs(parent) do
29 local delta
30 adj = s.indent and 0 or adj
31 local prev = parent[i - 1]
32 if not s.indent and not (prev and (s.d == "|" or prev.d == "|")) then
33 -- remove/add leading spaces
34 local off = prev and prev.finish + 1 or pstart
35 adj = adj + normalize_spacing(off, s.start - off - (prev and 1 or 0), deltas)
36 end
37 local nxt = parent[i + 1]
38 local is_last = not nxt
39 if (is_last and not keep_electric_space) or nxt and nxt.indent then
40 -- clean up trailing spaces
41 local has_eol = s.is_comment and parser.opposite[s.d] == "\n"
42 local off = is_last and parent.finish - (has_eol and i < #parent and 1 or 0) or nxt.start - nxt.indent - 1
43 local finish = s.finish + (has_eol and i < #parent and 0 or 1)
44 normalize_spacing(finish, off - finish, deltas)
45 end
46 if s.indent then
47 delta = base_indent - s.indent
48 local last_distinguished = fmt:last_distinguished(parent)
49 if last_distinguished then
50 delta = delta + 1
51 if i - 1 <= last_distinguished then
52 delta = delta + 2
53 end
54 elseif i == 1 then
55 -- remove leading space, if any
56 delta = -(s.start - pstart)
57 elseif -- align further arguments below the second one
58 parent.d == "(" -- [] and {} contain data, so no arguments
59 and not parent[1].is_string -- a string can't have arguments
60 --and not parent[1].is_list -- wrong, GNU Emacs compatible behaviour
61 then
62 if i > 2 and not parent[2].indent then
63 delta = base_indent + parent[2].start - pstart - s.indent
64 end
65 if delta < 0 then delta = math.max(-s.indent, delta) end
66 end
67 if delta ~= 0 then
68 table.insert(deltas, {s.start, delta})
69 end
70 adj = adj - delta
71 end
72 if s.is_list then
73 local nearest_indented = s.indent and s or parent.find_before(s, function(t) return t.indent end)
74 local parent_column = nearest_indented and
75 nearest_indented.indent + (s.start + (s.p and #s.p or 0) + #s.d) - nearest_indented.start
76 or base_indent + (s.start + (s.p and #s.p or 0) + #s.d) - pstart
77 refmt_list(s, parent_column - adj, padj + adj, deltas, keep_electric_space)
78 end
79 end
80 return deltas
81 end
83 local function refmt_at(scope, range, keep_electric_space)
84 if not range or not scope or scope.is_root then return end
85 local parent = walker.sexp_at(scope, true)
86 if not (parent and parent.is_list) then return end
87 local _, parent_at, m = walker.sexp_at(range, true)
88 if not m then
89 _, m = parent_at.after(range.start, startof)
90 if not m then
91 m = #parent_at
92 end
93 end
94 local base = parent_at[m] or parent_at
95 local path = walker.sexp_path(base)
96 local after_start, at_pfinish
97 if parent_at[m] then
98 local s = parent_at[#parent_at]
99 local has_eol = s.is_comment and parser.opposite[s.d] == "\n"
100 if range.start >= s.finish + 1 + (has_eol and 1 or 0) then
101 at_pfinish = true
102 else
103 after_start = range.start < base.start and 0 or range.start - base.start
105 else
106 after_start = (parent_at.p and #parent_at.p or 0) + #parent_at.d
108 local indented_parent = parent.indent and parent or parent.is_list and walker.indented_before(parent)
109 local parent_column = indented_parent and (indented_parent.indent + parent.start - indented_parent.start +
110 (parent.p and #parent.p or 0) + #parent.d) or
112 local deltas = refmt_list(parent, parent_column, 0, {}, keep_electric_space)
113 table.sort(deltas, function(d1, d2) return d1[1] > d2[1] end)
114 for _, pair in ipairs(deltas) do
115 local offset, delta = unpack(pair)
116 if delta > 0 then
117 write(offset, string.rep(" ", delta))
118 elseif delta < 0 then
119 delete(offset + delta, -delta)
122 parser.tree.rewind(parent.start or 0)
123 if path then
124 local sexp, parentng = walker.goto_path(path)
125 if sexp then
126 if at_pfinish then
127 return parentng.finish
128 else
129 return sexp.start + after_start
133 return range.start
136 local function splice(pos, sexps, skip, backwards, action)
137 local spliced
138 local sexp = walker.sexp_at(skip, true)
139 local start = sexp.start + (sexp.p and #sexp.p or 0)
140 local tosplice = action.splice or action.wrap or sexp.is_empty
141 local opening = (sexp.p or '')..sexp.d
142 local closing = parser.opposite[sexp.d]
143 local real_start = tosplice and sexp.start or start + #sexp.d
144 local splice_closing = closing ~= "\n" or sexp.is_empty
145 local real_finish = sexp.finish + 1 - (tosplice and splice_closing and 0 or #closing)
146 local first = backwards and
147 {start = real_start, finish = math.max(pos, start + #sexp.d)} or
148 {start = real_start, finish = sexp.is_empty and pos or start + #sexp.d}
149 local second = backwards and
150 {start = sexp.is_empty and pos or sexp.finish + 1 - #closing, finish = real_finish} or
151 {start = math.min(pos, sexp.finish + 1 - #closing), finish = real_finish}
152 action.func(second)
153 action.func(first)
154 spliced = tosplice
155 if action.kill then
156 local ndeleted = first.finish - first.start + second.finish - second.start
157 if ndeleted > 0 then
158 sexps.rewind(sexp.start)
161 return first.finish - first.start, spliced, opening, closing
164 local function pick_out(range, pos, action)
165 local sexps = parser.tree
166 local skips = sexps.unbalanced_delimiters(range)
167 -- handle splice and kill-splice of forms and strings:
168 if #skips == 1 then
169 local sexp = walker.sexp_at(skips[1], true)
170 local backward_splice = skips[1].opening and pos >= sexp.start + (sexp.p and #sexp.p or 0) + #sexp.d
171 and range.start >= sexp.start
172 local forward_splice = skips[1].closing and pos <= sexp.finish + 1 - #parser.opposite[sexp.d]
173 and range.finish <= sexp.finish + 1
174 if backward_splice or forward_splice then
175 return splice(backward_splice and range.finish or range.start, sexps, sexp, backward_splice, action)
178 local node, parent = walker.sexp_at({start = range.finish, finish = range.finish})
179 local drop_eol = node and node.finish + 1 == range.finish and node.is_comment and parser.opposite[node.d] == "\n"
180 table.sort(skips, function(a, b) return a.start < b.start end)
181 table.insert(skips, {start = range.finish - (drop_eol and 1 or 0)})
182 table.insert(skips, 1, {finish = range.start})
183 local ndeleted = drop_eol and 1 or 0
184 for i = #skips - 1, 1, -1 do
185 local region = {start = skips[i].finish, finish = skips[i + 1].start}
186 if skips[i].closing and skips[i + 1].opening then
187 -- leave out some of the space between adjacent lists
188 local _, rparent = walker.sexp_at(region)
189 local nxt = rparent.after(region.start, startof)
190 region.start = nxt and nxt.start or region.start
192 if action then
193 action.func(region)
194 ndeleted = ndeleted + (region.finish - region.start)
197 -- if parent[#parent + 1] is nil, we are at EOF
198 if ndeleted > 0 and (not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1]) then
199 sexps.rewind(range.start)
201 return ndeleted
204 local function raise_sexp(range, pos)
205 local sexp, parent = walker.sexp_at(range, true)
206 if sexp and parent and parent.is_list then
207 delete(sexp.finish + 1, parent.finish - sexp.finish)
208 delete(parent.start, sexp.start - parent.start)
209 parser.tree.rewind(parent.start)
210 range.start = parent.start + pos - sexp.start
211 range.finish = range.start
212 local _, nodes = walker.sexp_path(range)
213 local grandparent = nodes[#nodes - 2]
214 return grandparent and refmt_at(grandparent, range) or range.start
218 local function slurp_sexp(range, forward)
219 local _, parent = walker.sexp_at(range, true)
220 local seeker = forward and walker.finish_after or walker.start_before
221 if not parent or not parent.is_list then return range.start end
222 local r = {start = parent.start, finish = parent.finish + 1}
223 local newpos = seeker(r, is_comment)
224 if not newpos then return range.start end
225 local opening = (parent.p or '')..parent.d
226 local closing = parser.opposite[parent.d]
227 local delimiter = forward and closing or opening
228 if forward then
229 write(newpos, delimiter)
231 delete(forward and parent.finish or parent.start, #delimiter)
232 if not forward then
233 write(newpos, delimiter)
235 parser.tree.rewind(math.min(parent.start, newpos))
236 local _, parentng = walker.sexp_at(range, true)
237 return refmt_at(parentng, range)
240 local function barf_sexp(range, forward)
241 local _, parent, m = walker.sexp_at(range, true)
242 local seeker = forward and walker.finish_before or walker.start_after
243 -- TODO: barfing out of strings requires calling the parser on them
244 if not parent or not parent.is_list then return range.start end
245 local opening = (parent.p or '')..parent.d
246 local pstart = parent.start + #opening
247 local r = {start = forward and parent.finish - 1 or pstart, finish = forward and parent.finish or pstart + 1}
248 local newpos = seeker(r, is_comment) or forward and pstart or parent.finish
249 if not newpos then return range.start end
250 local closing = parser.opposite[parent.d]
251 local delimiter = forward and closing or opening
252 if not forward then
253 write(newpos, delimiter)
255 delete(forward and parent.finish or parent.start, #delimiter)
256 if forward then
257 write(newpos, delimiter)
259 parser.tree.rewind(math.min(parent.start, newpos))
260 local barfed_cursor_backward = m == 1 and not forward
261 local barfed_cursor_forward = m == #parent and forward
262 range.start = range.start + #delimiter * (barfed_cursor_backward and -1 or barfed_cursor_forward and 1 or 0)
263 local barfed_cursor = barfed_cursor_forward or barfed_cursor_backward
264 local _, nodes = walker.sexp_path(range)
265 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
266 local scope = barfed_cursor and parentng and not parentng.is_root and parentng or grandparent
267 return refmt_at(scope, range) or range.start
270 local function splice_sexp(range, _)
271 local _, parent = walker.sexp_at(range)
272 if not parent or not parent.d then return end
273 local opening = (parent.p or '')..parent.d
274 local closing = parser.opposite[parent.d]
275 local finish = parent.finish + 1 - #closing
276 if closing ~= "\n" then
277 delete(finish, #closing)
279 -- TODO: (un)escape special characters, if necessary
280 delete(parent.start, parent.is_empty and (finish - parent.start) or #opening)
281 parser.tree.rewind(parent.start)
282 range.start = range.start - #opening
283 range.finish = range.start
284 local _, parentng = walker.sexp_at(range, true)
285 return refmt_at(parentng, range)
288 local function rewrap(parent, kind)
289 local pstart = parent.start + #((parent.p or '')..parent.d) - 1
290 delete(parent.finish, 1)
291 write(parent.finish, parser.opposite[kind])
292 delete(pstart, #parent.d)
293 write(pstart, kind)
294 parser.tree.rewind(parent.start)
297 local function cycle_wrap(range, pos)
298 local _, parent = walker.sexp_at(range)
299 if not parent or not parent.is_list then return end
300 local next_kind = {["("] = "[", ["["] = "{", ["{"] = "("}
301 rewrap(parent, next_kind[parent.d])
302 return pos
305 local function split_sexp(range)
306 local _, parent = walker.sexp_at(range)
307 if not (parent and parent.d) then return end
308 local new_finish, new_start
309 if parent.is_list then
310 local prev = parent.before(range.start, finishof, is_comment)
311 new_finish = prev and prev.finish + 1
312 -- XXX: do not skip comments here, so they end up in the second list
313 -- and are not separated from their target expression:
314 local nxt = new_finish and parent.after(new_finish, startof)
315 new_start = nxt and nxt.start
316 else
317 new_start = range.start
318 new_finish = range.start
320 if not (new_start and new_finish) then return end
321 local opening = (parent.p or '')..parent.d
322 local closing = parser.opposite[parent.d]
323 write(new_start, opening)
324 local sep = parser.opposite[parent.d] == "\n" and "" -- line comments already have a separator
325 or new_finish == new_start and " " -- only add a separator if there was none before
326 or ""
327 write(new_finish, closing..sep)
328 parser.tree.rewind(parent.start)
329 range.start = new_start + (parent.is_list and 0 or #opening + #closing)
330 range.finish = range.start
331 local _, nodes = walker.sexp_path(range)
332 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
333 local scope = parentng and not parentng.is_root and parentng or grandparent
334 return refmt_at(scope, range) or range.start
337 local function join_sexps(range)
338 local node, parent = walker.sexp_at(range, true)
339 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
340 local second = first ~= node and node or parent.after(range.start, startof)
341 if not (first and second and first.d and
342 (first.d == second.d or
343 -- join line comments even when their delimiters differ slightly
344 -- (different number of semicolons, existence/lack of a space after them)
345 parser.opposite[first.d] == parser.opposite[second.d])) then
346 return
348 local opening = (second.p or '')..second.d
349 local closing = parser.opposite[first.d]
350 local pos
351 if not first.is_list then
352 pos = first.finish + 1 - #closing
353 delete(pos, second.start + #opening - pos)
354 else
355 delete(second.start, #opening)
356 delete(first.finish, #closing)
357 pos = second.start - #closing
359 parser.tree.rewind(first.start)
360 range.start = pos
361 range.finish = range.start
362 local _, nodes = walker.sexp_path(range)
363 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
364 local scope = parentng and not parentng.is_root and parentng or grandparent
365 return refmt_at(scope, range) or range.start
368 local function delete_splicing(range, pos, splicing, delete_and_yank)
369 local action = {kill = true, wrap = splicing, splice = splicing, func = delete_and_yank}
370 local sexp, parent, n = walker.sexp_at(range, true)
371 local ndeleted, spliced = pick_out(range, pos, action)
372 local closing = spliced and parser.opposite[sexp.d]
373 local inner_list_len = spliced and sexp.finish - sexp.start + 1 - #sexp.d - #closing
374 local range_len = range.finish - range.start
375 local backwards = pos == range.finish
376 local whole_object = sexp and
377 (sexp.start == range.start and sexp.finish + 1 == range.finish
378 or spliced and (inner_list_len <= (backwards and range_len + ndeleted or range_len)))
379 local in_head_atom = sexp and not sexp.d and n == 1 and #parent > 1
380 local in_whitespace = not sexp
381 if whole_object or in_whitespace or in_head_atom then
382 local cur = whole_object and sexp.start or range.start
383 -- if parent[#parent + 1] is nil, we are at EOF
384 if not parent.is_root or parent.is_parsed(cur) or parent[#parent + 1] then
385 parser.tree.rewind(cur)
387 local r = {start = cur, finish = cur}
388 local _, parentng = walker.sexp_at(r, true)
389 local newpos = refmt_at(parentng, r)
390 return newpos or cur
392 return spliced and (backwards and sexp.start or range.start - ndeleted)
393 or not backwards and ndeleted <= 0 and range.finish - ndeleted
394 or range.start
397 local function transpose(range, first, second)
398 if not (first and second) then return end
399 local copy1 = first.text
400 local copy2 = second.text
401 delete(second.start, second.finish + 1 - second.start)
402 write(second.start, copy1)
403 delete(first.start, first.finish + 1 - first.start)
404 write(first.start, copy2)
405 parser.tree.rewind(first.start)
406 range.start = second.finish + 1
407 range.finish = range.start
408 local _, parentng = walker.sexp_at(range, true)
409 return refmt_at(parentng, range) or range.start
412 local function transpose_sexps(range)
413 local node, parent = walker.sexp_at(range, true)
414 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
415 local second = first ~= node and node or parent.after(range.start, startof)
416 return transpose(range, first, second)
419 local function transpose_words(range)
420 local _, first = walker.start_float_before(range)
421 local _, second = walker.finish_float_after(range)
422 if first and second and first.start == second.start then
423 _, first = walker.start_float_before(first)
425 return transpose(range, first, second)
428 local function transpose_chars(range)
429 local node, parent, i = walker.sexp_at(range)
430 local nxt = i and parent[i + 1]
431 local pstart = not parent.is_root and parent.start + (parent.p and #parent.p or 0) + #parent.d
432 local pfinish = not parent.is_root and parent.finish
433 local npref = node and node.p and node.start + #node.p
434 -- only allow transposing while inside atoms/words and prefixes
435 if node and ((npref and (range.start <= npref and range.start > node.start) or
436 node.d and range.start > node.finish + 1) or not node.d and (not pstart or range.start > pstart)) then
437 local start = range.start -
438 ((range.start == pfinish or range.start == npref or
439 range.start == node.finish + 1 and (parent.is_list or parent.is_root) and (not nxt or nxt.indent)) and 1 or 0)
440 local str_start = start - node.start + 1
441 local char = node.text:sub(str_start, str_start)
442 delete(start, 1)
443 write(start - 1, #char > 0 and char or " ")
444 parser.tree.rewind(start)
445 local in_head_atom = i == 1 and #parent > 1
446 return parent.is_list and in_head_atom
447 and refmt_at(parent, {start = start + 1, finish = start + 1})
448 or start + 1
452 local function delete_nonsplicing(range, pos, delete_maybe_yank)
453 local action = {kill = true, wrap = false, splice = false, func = delete_maybe_yank}
454 local ndeleted, spliced, opening = pick_out(range, pos, action)
455 local backwards = pos == range.finish
456 if opening then
457 if spliced then
458 return pos - ndeleted
459 else
460 if ndeleted == 0 then
461 local closing = parser.opposite[opening]
462 if closing == "\n" then
463 local sexp = walker.sexp_at(range)
464 if pos == sexp.start + #sexp.d then
465 return join_sexps(range) or pos
466 elseif pos == sexp.finish then
467 local r = {start = pos + #closing, finish = pos + #closing}
468 return join_sexps(r) or pos + #closing
472 return backwards and (range.finish - ndeleted) or range.start
474 else
475 return backwards and range.start or (range.finish - ndeleted)
479 local function make_wrap(kind)
480 return function(range, pos, auto_square)
481 local opening = kind
482 if kind == ";" then
483 local node, parent = walker.sexp_at(range, true)
484 if parent.is_root then
485 -- FIXME: don't do that at the end of a defun
486 opening = ";;; "
487 else
488 local nxt = parent.after(range.finish, startof)
489 if not (nxt and nxt.indent and node and (node.finish + 1 <= range.start)) then
490 opening = ";; "
494 local function _wrap(r)
495 local indices, nodes = walker.sexp_path(r)
496 local delimiter, closing = opening, parser.opposite[opening]
497 local squarewords = fmt.squarewords
498 if squarewords and (auto_square or squarewords.not_optional)
499 and not fmt:adjust_bracket_p(indices, nodes, r) then
500 delimiter, closing = "[", "]"
502 write(r.finish, closing)
503 write(r.start, delimiter)
505 local action = {kill = false, wrap = false, splice = false, func = _wrap}
506 pick_out(range, pos, action)
507 parser.tree.rewind(range.start)
508 local _, parentng = walker.sexp_at(range, true)
509 range.start = range.start + #opening
510 return refmt_at(parentng, range) or range.start
514 local function newline(parent, range)
515 if not parent.is_list then
516 if parent.d and parser.opposite[parent.d] == "\n" and range.start < parent.finish + 1 then
517 local newpos = split_sexp(range)
518 if newpos then
519 return newpos
522 -- if parent[#parent + 1] is nil, we are at EOF
523 if not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
524 parser.tree.rewind(parent.start or range.start)
526 return
528 local last_nonblank_in_list = parent[#parent] and parent[#parent].finish or parent.start
529 local nxt = parent.is_list and parent.after(range.start, startof)
530 local last_on_line = nxt and nxt.indent and nxt.start > range.start
531 local after_last = range.start > last_nonblank_in_list
532 local placeholder = "asdf"
533 write(range.start, "\n"..((after_last or last_on_line) and placeholder or ""))
534 parser.tree.rewind(parent.start or range.start)
535 -- move the cursor onto the placeholder, so refmt_at can restore the position:
536 nxt = parent.is_list and walker.start_after(range) or range.start + 1
537 local rangeng = {start = nxt, finish = nxt}
538 local newpos = refmt_at(parent, rangeng) or rangeng.start
539 if after_last then
540 local _, parentng = walker.sexp_at({start = newpos, finish = newpos}, true)
541 local autoindent = parentng[#parentng].indent
542 write(parentng.finish, "\n"..string.rep(" ", autoindent or 0))
544 if after_last or last_on_line then
545 delete(newpos, #placeholder)
547 parser.tree.rewind(newpos)
548 return newpos
551 local function close_and_newline(range)
552 local _, parent = walker.sexp_at(range)
553 if parent and not parent.is_root then
554 local r = {start = parent.finish, finish = parent.finish}
555 local newpos = refmt_at(parent, r)
556 r = {start = (newpos or r.finish) + 1, finish = (newpos or r.finish) + 1}
557 local list, parentng = walker.sexp_at(r)
558 newpos = newline(parentng, {start = list.finish + 1, finish = list.finish + 1})
559 if not newpos then
560 newpos = list.finish + 1
561 write(newpos, "\n")
563 return newpos
565 return range.start
568 return {
569 delete_splicing = delete_splicing,
570 delete_nonsplicing = delete_nonsplicing,
571 refmt_at = refmt_at,
572 pick_out = pick_out,
573 raise_sexp = raise_sexp,
574 slurp_sexp = slurp_sexp,
575 barf_sexp = barf_sexp,
576 splice_sexp = splice_sexp,
577 wrap_round = make_wrap("("),
578 meta_doublequote = make_wrap('"'),
579 comment_dwim = make_wrap(";"),
580 newline = newline,
581 close_and_newline = close_and_newline,
582 cycle_wrap = cycle_wrap,
583 split_sexp = split_sexp,
584 join_sexps = join_sexps,
585 transpose_sexps = transpose_sexps,
586 transpose_words = transpose_words,
587 transpose_chars = transpose_chars,
591 return M