add copyright notice
[lisp-parkour.git] / edit.lua
blob2c6a3347858d74eb932600ee1e862e94dc67df5d
1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
4 local M = {}
6 -- XXX: in Lua 5.2 unpack() was moved into table
7 local unpack = table.unpack or unpack
9 local function startof(node) return node.start end
10 local function finishof(node) return node.finish end
11 local function is_comment(node) return node.is_comment end
13 local function normalize_spacing(start, delta, list)
14 if delta > 0 then
15 table.insert(list, {start + delta, -delta})
16 elseif delta < 0 then
17 table.insert(list, {start, -delta})
18 end
19 return delta
20 end
22 local function leading_space(s, deltas, adj, parent, i, pstart, has_eol)
23 local prev = parent[i - 1]
24 if not s.indent and not (prev and (s.d == "|" or prev.d == "|")) and not has_eol then
25 local off = prev and prev.finish + 1 or pstart
26 adj = adj + normalize_spacing(off, s.start - off - (prev and 1 or 0), deltas)
27 end
28 return adj
29 end
31 local function trailing_space(s, deltas, adj, parent, i, has_eol, keep_electric_space)
32 local nxt = parent[i + 1]
33 local is_last = not nxt
34 if (is_last and not keep_electric_space) or nxt and nxt.indent then
35 local off = is_last and parent.finish - (has_eol and i < #parent and 1 or 0) or nxt.start - nxt.indent - 1
36 local finish = s.finish + (has_eol and i < #parent and 0 or 1)
37 normalize_spacing(finish, off - finish, deltas)
38 end
39 return adj
40 end
42 local function indentation(s, deltas, adj, parent, i, pstart, base_indent, last_distinguished, first_noncomment_argument,
43 op_adj, indent_adj)
44 if s.indent then
45 local delta = base_indent - s.indent
46 local firstarg_delta = base_indent + parent[1].finish + 2 - pstart - s.indent - op_adj - (indent_adj or 0)
47 if last_distinguished and i > 1 then
48 delta = delta + 1
49 if i - 1 <= last_distinguished then
50 if parent[first_noncomment_argument + 1].indent then
51 delta = delta + 2
52 else
53 delta = firstarg_delta
54 end
55 elseif last_distinguished < 0
56 and i > first_noncomment_argument + 1 and not parent[first_noncomment_argument + 1].indent then
57 delta = firstarg_delta
58 end
59 elseif i == 1 then
60 -- remove leading space
61 delta = -(s.start - pstart)
62 elseif -- align further arguments below the second one
63 parent.d == "(" -- [] and {} contain data, so no arguments
64 and not parent[1].is_string -- a string can't have arguments
65 --and not parent[1].is_list -- wrong, GNU Emacs compatible behaviour
66 then
67 if i > first_noncomment_argument + 1 and not parent[first_noncomment_argument + 1].indent then
68 delta = firstarg_delta
69 end
70 if delta < 0 then delta = math.max(-s.indent, delta) end
71 end
72 if delta ~= 0 then
73 table.insert(deltas, {s.start, delta})
74 end
75 adj = adj - delta
76 end
77 return adj
78 end
80 local function is_lone_prefix(node)
81 return node and node.p and (#node.p == node.finish - node.start + 1)
82 end
84 function M.new(parser, walker, fmt, write, delete, eol_at)
86 local function refmt_list(parent, base_indent, padj, deltas, keep_electric_space)
87 local adj = 0
88 local op_adj = 0
89 local indent_adj = 0
90 local pstart = parent.start + (parent.p and #parent.p or 0) + #parent.d
91 if parent.is_empty then
92 normalize_spacing(pstart, parent.finish - pstart, deltas)
93 return deltas
94 end
95 local last_distinguished = fmt:last_distinguished(parent)
96 local first_noncomment_argument = 1
97 if #parent > 2 and parent[1].is_list and #parent[1] > 1 then
98 local op = parent[1]
99 local nearest_indented = op[#op].indent and op[#op] or walker.indented_before(op[#op])
100 indent_adj = nearest_indented.start - pstart - nearest_indented.indent + 1
102 for i, s in ipairs(parent) do
103 adj = s.indent and 0 or adj
104 if last_distinguished then
105 -- do not let early comments influence the indentation of real expressions:
106 if i <= last_distinguished and s.is_comment then
107 last_distinguished = last_distinguished + 1
109 -- s is the name of a named let. account for it:
110 if i == last_distinguished + 1 and not s.is_list and parent[1].text == "let" then
111 last_distinguished = last_distinguished + 1
113 -- do not let early comments influence the indentation of real expressions:
114 elseif i <= first_noncomment_argument + 1 and s.is_comment then
115 first_noncomment_argument = first_noncomment_argument + 1
117 local has_eol = s.is_line_comment
118 adj = leading_space(s, deltas, adj, parent, i, pstart, has_eol)
119 adj = trailing_space(s, deltas, adj, parent, i, has_eol, keep_electric_space)
120 adj = indentation(s, deltas, adj, parent, i, pstart, base_indent, last_distinguished, first_noncomment_argument,
121 op_adj, indent_adj)
122 if i == 1 then
123 op_adj = adj
125 if s.is_list then
126 local nearest_indented = s.indent and s or walker.indented_before(s)
127 local parent_column = nearest_indented and
128 nearest_indented.indent + (s.start + (s.p and #s.p or 0) + #s.d) - nearest_indented.start
129 or base_indent + (s.start + (s.p and #s.p or 0) + #s.d) - pstart
130 refmt_list(s, parent_column - adj, padj + adj, deltas, keep_electric_space)
133 return deltas
136 local function path_from(range)
137 local _, parent_at, m = walker.sexp_at(range, true)
138 local path
139 if parent_at[m] then
140 path = walker.sexp_path(parent_at[m])
141 local base = parent_at[m]
142 if range.finish == base.finish + 1 then
143 path.after_finish = 0 -- lists can end up with a different length
144 else
145 path.after_start = range.start - base.start
147 else
148 local s = parent_at[#parent_at]
149 if not s or range.start >= s.finish + 1 + (s.is_line_comment and 1 or 0) then
150 path = walker.sexp_path(parent_at)
151 path.at_pfinish = true
152 else
153 local prev, n = parent_at.before(range.start, finishof)
154 if prev then
155 path = walker.sexp_path(prev)
156 local nxt = parent_at[n + 1]
157 path.after_finish = nxt and not nxt.indent and 1 or range.finish - prev.finish - 1
158 else
159 path = walker.sexp_path(parent_at[1])
160 path.after_start = 0
164 return path
167 local function pos_from(path, range)
168 local sexp, parentng, n = walker.goto_path(path)
169 if sexp then
170 if path.after_finish then
171 local nxt = n and parentng[n + 1]
172 local max = nxt and nxt.start - sexp.finish - 1
173 return sexp.finish + 1 - (range.finish - range.start) +
174 (max and math.min(path.after_finish, max) or path.after_finish)
175 elseif path.at_pfinish then
176 return sexp.finish
177 else
178 return sexp.start + path.after_start
183 local function refmt_at(scope, range, keep_electric_space)
184 if not range or not scope or scope.is_root or (scope.finish - scope.start < 2) then return range.start end
185 local parent = walker.sexp_at(scope, true)
186 if not (parent and parent.is_list) then return range.start end
187 local path = path_from(range)
188 local indented_parent = parent.indent and parent or parent.is_list and walker.indented_before(parent)
189 local parent_column = indented_parent and (indented_parent.indent + parent.start - indented_parent.start +
190 (parent.p and #parent.p or 0) + #parent.d) or
192 local deltas = refmt_list(parent, parent_column, 0, {}, keep_electric_space)
193 table.sort(deltas, function(d1, d2) return d1[1] > d2[1] end)
194 for _, pair in ipairs(deltas) do
195 local offset, delta = unpack(pair)
196 if delta > 0 then
197 write(offset, string.rep(" ", delta))
198 elseif delta < 0 then
199 delete(offset + delta, -delta)
202 parser.tree.rewind(parent.start or 0)
203 return path and pos_from(path, range) or range.start
206 local function splice(pos, sexps, skip, backwards, action)
207 local spliced
208 local sexp = walker.sexp_at(skip, true)
209 local start = sexp.start + (sexp.p and #sexp.p or 0)
210 -- XXX: don't splice empty line comments _yet_. See _join_or_splice
211 local tosplice = action.splice or action.wrap or not sexp.is_line_comment and sexp.is_empty
212 local opening = (sexp.p or "")..sexp.d
213 local closing = parser.opposite[sexp.d]
214 local real_start = tosplice and sexp.start or start + #sexp.d
215 local splice_closing = not sexp.is_line_comment or sexp.is_empty
216 local real_finish = sexp.finish + 1 - (tosplice and splice_closing and 0 or #closing)
217 local first = backwards and
218 {start = real_start, finish = math.max(pos, start + #sexp.d)} or
219 {start = real_start, finish = sexp.is_empty and pos or start + #sexp.d}
220 local second = backwards and
221 {start = sexp.is_empty and pos or sexp.finish + 1 - #closing, finish = real_finish} or
222 {start = math.min(pos, sexp.finish + 1 - #closing), finish = real_finish}
223 action.func(second)
224 action.func(first)
225 spliced = tosplice
226 if action.kill then
227 local ndeleted = first.finish - first.start + second.finish - second.start
228 if ndeleted > 0 then
229 sexps.rewind(sexp.start)
232 return first.finish - first.start, spliced, opening, closing
236 -- This function handles some whitespace-only deletion corner cases magically.
237 -- It does so by
238 -- a) extending or shrinking the range that is to be deleted
239 -- b) returning an integer to trick pick_out to glide the cursor, but calling refmt to restore the deleted spaces
240 -- c) both a) and b)
241 local function delete_whitespace_only(range, pos)
242 local node, parent = walker.sexp_at(range)
243 if node or not parent.is_list then return end
244 local prev = parent.before(pos, finishof)
245 local nxt = parent.after(pos, startof)
246 if prev and prev.finish >= range.start or nxt and nxt.start < range.finish then return end
247 local backwards = pos == range.finish
248 local adj = 0
249 local eol = eol_at(pos)
250 local on_empty_line = not nxt or eol and nxt.start > eol
251 local empty_line_after_comment = prev and prev.is_line_comment
252 and on_empty_line
253 if nxt and nxt.indent then
254 if backwards and prev then
255 -- join current line with the previous one, unless the latter is a line comment
256 range.start = prev.finish + (empty_line_after_comment and 0 or 1)
257 if not prev.d then
258 range.finish = range.start + 1
259 adj = (on_empty_line and 0 or 1)
261 else
262 adj = nxt.start - range.finish
264 return adj
266 if not nxt and prev then
267 -- clean up trailing whitespace (e.g. electric RET)
268 range.start = prev.finish + 1
269 range.finish = parent.finish
270 elseif nxt and nxt.start == range.finish and (not prev or prev.finish + 1 == range.start) then
271 if prev and prev.d or nxt and nxt.d and not is_lone_prefix(prev) then
272 -- don't delete spaces near delimiters, just slide the cursor:
273 if backwards then
274 range.finish = range.start
275 else
276 range.start = range.finish
279 return adj
283 local function big_enough_parent(pos1, pos2)
284 -- since the pos1-pos2 range can cross list boundaries, find which list contains both pos1 and pos2
285 local _, p1, p2
286 _, p1 = walker.sexp_at({start = pos1, finish = pos1}, true)
287 if p1.is_root or not (p1.start < pos1 and p1.finish > pos2) then
288 _, p2 = walker.sexp_at({start = pos2, finish = pos2}, true)
290 return p2 and not p2.is_root and p2 or p1
293 local function extend_overlap(range)
294 local rnode = walker.sexp_at({start = range.finish, finish = range.finish})
295 if rnode and rnode.p and rnode.p:find";"
296 and range.finish > rnode.start and range.finish < rnode.start + #rnode.p then
297 return {start = range.start, finish = rnode.start + #rnode.p}
299 return range
302 local function pick_out(range, pos, action)
303 local ndeleted = 0
304 if range.start == range.finish then return ndeleted end
305 local sexps = parser.tree
306 local skips = sexps.unbalanced_delimiters(range)
307 -- handle splice and kill-splice of forms and strings:
308 if #skips == 1 then
309 local sexp = walker.sexp_at(skips[1], true)
310 local backward_splice = skips[1].opening and pos >= sexp.start + (sexp.p and #sexp.p or 0) + #sexp.d
311 and range.start >= sexp.start
312 local forward_splice = skips[1].closing and pos <= sexp.finish + 1 - #parser.opposite[sexp.d]
313 and range.finish <= sexp.finish + 1
314 if backward_splice or forward_splice then
315 return splice(backward_splice and range.finish or range.start, sexps, sexp, backward_splice, action)
318 local node, parent = walker.sexp_at({start = range.finish, finish = range.finish})
319 -- if the range ends with a line comment, don't delete its closing newline:
320 local drop_eol = action.kill and node and
321 node.finish + 1 == range.finish and node.is_line_comment
322 local par = big_enough_parent(range.start, range.finish)
323 local operator_changed = par[1] and par[1].finish >= range.start
324 local refmt = #skips == 0 and delete_whitespace_only(range, pos) or operator_changed and 0
325 table.sort(skips, function(a, b) return a.start < b.start end)
326 table.insert(skips, {start = range.finish - (drop_eol and 1 or 0)})
327 table.insert(skips, 1, {finish = range.start})
328 ndeleted = ndeleted + (drop_eol and 1 or 0)
329 for i = #skips - 1, 1, -1 do
330 local region = {start = skips[i].finish, finish = skips[i + 1].start}
331 if skips[i].closing and skips[i + 1].opening then
332 -- leave out some of the space between adjacent lists
333 local _, rparent = walker.sexp_at(region)
334 local nxt = rparent.after(region.start, startof)
335 region.start = nxt and nxt.start or region.start
337 if action then
338 action.func(region)
339 ndeleted = ndeleted + (region.finish - region.start)
342 -- if parent[#parent + 1] is nil, we are at EOF
343 if ndeleted > 0 and (not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1]) then
344 sexps.rewind(range.start)
346 return ndeleted - (refmt or 0), nil, nil, nil, refmt
349 local function raise_sexp(range, pos)
350 local sexp, parent = walker.sexp_at(range, true)
351 if sexp and parent and parent.is_list then
352 delete(sexp.finish + 1, parent.finish - sexp.finish)
353 delete(parent.start, sexp.start - parent.start)
354 parser.tree.rewind(parent.start)
355 range.start = parent.start + pos - sexp.start
356 range.finish = range.start
357 local _, nodes = walker.sexp_path(range)
358 local grandparent = nodes[#nodes - 2]
359 return grandparent and refmt_at(grandparent, range) or range.start
363 local function slurp_sexp(range, forward)
364 local _, parent = walker.sexp_at(range, true)
365 local seeker = forward and walker.finish_after or walker.start_before
366 if not parent or not parent.is_list then return range.start end
367 local r = {start = parent.start, finish = parent.finish + 1}
368 local newpos = seeker(r, is_comment)
369 if not newpos then return range.start end
370 local opening = (parent.p or "")..parent.d
371 local closing = parser.opposite[parent.d]
372 local delimiter = forward and closing or opening
373 if forward then
374 write(newpos, delimiter)
376 delete(forward and parent.finish or parent.start, #delimiter)
377 if not forward then
378 write(newpos, delimiter)
380 parser.tree.rewind(math.min(parent.start, newpos))
381 return refmt_at(big_enough_parent(newpos, range.start), range)
384 local function barf_sexp(range, forward)
385 local _, parent = walker.sexp_at(range, true)
386 local seeker = forward and walker.finish_before or walker.start_after
387 -- TODO: barfing out of strings requires calling the parser on them
388 if not parent or not parent.is_list or parent.is_empty then return range.start end
389 local opening = (parent.p or "")..parent.d
390 local pstart = parent.start + #opening
391 local r = {start = forward and parent.finish - 1 or pstart, finish = forward and parent.finish or pstart + 1}
392 local newpos = seeker(r, is_comment) or forward and pstart or parent.finish
393 local closing = parser.opposite[parent.d]
394 local delimiter = forward and closing or opening
395 if not forward then
396 write(newpos, delimiter)
398 delete(forward and parent.finish or parent.start, #delimiter)
399 if forward then
400 write(newpos, delimiter)
402 parser.tree.rewind(math.min(parent.start, newpos))
403 local drag = forward and (newpos < range.finish) or not forward and (newpos > range.start)
404 local keep_inside = forward and #parent > 1 and range.finish > range.start and 1 or 0
405 newpos = drag and newpos - keep_inside or range.start
406 local rangeng = {start = newpos, finish = newpos}
407 return refmt_at(big_enough_parent(newpos, parent.finish + 1), rangeng)
410 local function splice_anylist(range, _, no_refmt)
411 local _, parent = walker.sexp_at(range)
412 if not parent or not parent.d then return end
413 local opening = (parent.p or "")..parent.d
414 local closing = parser.opposite[parent.d]
415 local finish = parent.finish + 1 - #closing
416 if not parent.is_line_comment then
417 delete(finish, #closing)
419 -- TODO: (un)escape special characters, if necessary
420 delete(parent.start, parent.is_empty and (finish - parent.start) or #opening)
421 parser.tree.rewind(parent.start)
422 range.start = parent.start
423 range.finish = range.start
424 local _, parentng = walker.sexp_at(range, true)
425 return not no_refmt and refmt_at(parentng, range) or range.start
428 local function rewrap(parent, kind)
429 local pstart = parent.start + #((parent.p or "")..parent.d) - 1
430 delete(parent.finish, 1)
431 write(parent.finish, parser.opposite[kind])
432 delete(pstart, #parent.d)
433 write(pstart, kind)
434 parser.tree.rewind(parent.start)
437 local function cycle_wrap(range, pos)
438 local _, parent = walker.sexp_at(range)
439 if not parent or not parent.is_list then return end
440 local next_kind = {["("] = "[", ["["] = "{", ["{"] = "("}
441 rewrap(parent, next_kind[parent.d])
442 return refmt_at(parent, range) or pos
445 local function split_anylist(range)
446 local _, parent = walker.sexp_at(range)
447 if not (parent and parent.d) then return end
448 local new_finish, new_start
449 if parent.is_list then
450 local prev = parent.before(range.start, finishof, is_comment)
451 new_finish = prev and prev.finish + 1
452 -- XXX: do not skip comments here, so they end up in the second list
453 -- and are not separated from their target expression:
454 local nxt = new_finish and parent.after(new_finish, startof)
455 new_start = nxt and nxt.start
456 else
457 new_start = range.start
458 new_finish = range.start
460 if not (new_start and new_finish) then return end
461 local opening = (parent.p or "")..parent.d
462 local closing = parser.opposite[parent.d]
463 write(new_start, opening)
464 local sep = parent.is_line_comment and "" -- line comments already have a separator
465 or new_finish == new_start and " " -- only add a separator if there was none before
466 or ""
467 write(new_finish, closing..sep)
468 parser.tree.rewind(parent.start)
469 range.start = new_start + (parent.is_list and 0 or #opening + #closing)
470 range.finish = range.start
471 local _, nodes = walker.sexp_path(range)
472 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
473 local scope = parentng and not parentng.is_root and parentng or grandparent
474 return refmt_at(scope, range)
477 local function join_anylists(range)
478 local node, parent = walker.sexp_at(range, true)
479 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
480 local second = first ~= node and node or parent.after(range.start, startof)
481 if not (first and second and first.d and
482 -- don't join line comments to margin comments:
483 (not first.is_line_comment or first.indent and second.indent) and
484 (first.d == second.d or
485 -- join line comments even when their delimiters differ slightly
486 -- (different number of semicolons, existence/lack of a space after them)
487 parser.opposite[first.d] == parser.opposite[second.d])) then
488 return
490 local opening = (second.p or "")..second.d
491 local closing = parser.opposite[first.d]
492 local pos
493 if not first.is_list then
494 pos = first.finish + 1 - #closing
495 delete(pos, second.start + #opening - pos)
496 else
497 delete(second.start, #opening)
498 delete(first.finish, #closing)
499 pos = second.start - #closing
501 parser.tree.rewind(first.start)
502 range.start = pos
503 range.finish = range.start
504 local _, nodes = walker.sexp_path(range)
505 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
506 local scope = parentng and not parentng.is_root and parentng or grandparent
507 return refmt_at(scope, range)
510 local function delete_splicing(range, pos, splicing, delete_and_yank)
511 local action = {kill = true, wrap = splicing, splice = splicing, func = delete_and_yank}
512 local sexp, parent, n = walker.sexp_at(range)
513 local nxt = n and parent[n + 1]
514 local prev = n and parent[n - 1]
515 range = extend_overlap(range)
516 local ndeleted, spliced, opening, closing = pick_out(range, pos, action)
517 local inner_list_len = spliced and sexp.finish - sexp.start + 1 - #opening - #closing
518 local range_len = range.finish - range.start
519 local backwards = pos == range.finish
520 local whole_object = sexp and
521 (sexp.start == range.start and sexp.finish + 1 == range.finish
522 or spliced and inner_list_len <= range_len - (backwards and #opening or #closing))
523 local in_head_atom = sexp and not sexp.d and n == 1 and #parent > 1
524 local in_whitespace = not sexp
525 if whole_object or spliced or in_whitespace or in_head_atom then
526 local cur = (whole_object or spliced) and (prev and prev.finish or sexp.start) or range.start
527 -- if parent[#parent + 1] is nil, we are at EOF
528 if not parent.is_root or parent.is_parsed(cur) or parent[#parent + 1] then
529 parser.tree.rewind(cur)
531 -- make sure the cursor is not left in whitespace:
532 if whole_object and nxt then
533 cur = nxt.start - range_len - (spliced and (backwards and #closing or #opening) or 0)
534 elseif whole_object and prev then
535 cur = prev.finish
536 elseif whole_object and parent.d and not parent.is_list then
537 local _, parentng = walker.sexp_at({start = cur, finish = cur})
538 local trailing = parentng.spaces_after(sexp.start)
539 if trailing then
540 delete(sexp.start, trailing - sexp.start)
541 parser.tree.rewind(sexp.start)
542 local c = parser.opposite[parentng.d]
543 if trailing == parentng.finish + 1 - #c then
544 local leading = parentng.spaces_before(sexp.start)
545 -- keep the cursor inside the parent
546 cur = (leading or cur) - 1
548 else
549 local leading = parentng.spaces_before(sexp.start)
550 if leading then
551 delete(leading, sexp.start - leading)
552 parser.tree.rewind(leading)
553 -- keep the cursor inside the parent
554 cur = leading - 1
557 elseif spliced then
558 cur = (backwards and sexp.start or (nxt and nxt.start - range_len or range.start) - ndeleted)
560 local r = {start = cur, finish = cur + 1}
561 local _, parentng = walker.sexp_at(r, true)
562 return refmt_at(parentng, r)
564 return not backwards and ndeleted <= 0 and range.finish - ndeleted
565 or backwards and ndeleted <= 0 and range.start + ndeleted
566 or range.start
569 local function transpose(range, first, second)
570 if not (first and second) then return end
571 local copy1 = first.text
572 local copy2 = second.text
573 delete(second.start, second.finish + 1 - second.start)
574 write(second.start, copy1)
575 delete(first.start, first.finish + 1 - first.start)
576 write(first.start, copy2)
577 parser.tree.rewind(first.start)
578 range.start = second.finish + 1
579 range.finish = range.start
580 return refmt_at(big_enough_parent(first.start, second.finish), range)
583 local function transpose_sexps(range)
584 local node, parent = walker.sexp_at(range, true)
585 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
586 local second = first ~= node and node or parent.after(range.start, startof)
587 return transpose(range, first, second)
590 local function transpose_words(range)
591 local first, second, _
592 first = walker.sexp_at(range)
593 if not first or first.d or first.start == range.finish then
594 _, first = walker.start_float_before(range)
596 if first then
597 _, second = walker.finish_float_after({start = first.finish + 1, finish = first.finish + 1})
599 return transpose(range, first, second)
602 local function transpose_chars(range)
603 local node, parent, i = walker.sexp_at(range)
604 local nxt = i and parent[i + 1]
605 local pstart = not parent.is_root and parent.start + (parent.p and #parent.p or 0) + #parent.d
606 local pfinish = not parent.is_root and parent.finish
607 local npref = node and node.p and node.start + #node.p
608 -- only allow transposing while inside atoms/words and prefixes
609 if node and ((npref and (range.start <= npref and range.start > node.start) or
610 node.d and range.start > node.finish + 1) or not node.d and (not pstart or range.start > pstart)) then
611 local start = range.start -
612 ((range.start == pfinish or range.start == npref or
613 range.start == node.finish + 1 and (parent.is_list or parent.is_root) and (not nxt or nxt.indent)) and 1 or 0)
614 local str_start = start - node.start + 1
615 local char = node.text:sub(str_start, str_start)
616 delete(start, 1)
617 write(start - 1, #char > 0 and char or " ")
618 parser.tree.rewind(start)
619 local in_head_atom = i == 1 and #parent > 1
620 return parent.is_list and in_head_atom and refmt_at(parent, {start = start + 1, finish = start + 1}) or start + 1
624 local function _join_or_splice(parent, n, range, pos)
625 local sexp = parent[n]
626 local nxt = parent[n + 1]
627 local is_last = not nxt or not nxt.is_line_comment
628 local newpos = not (is_last and sexp.is_empty) and join_anylists(range)
629 if not newpos and sexp.is_empty then
630 newpos = splice_anylist({start = pos, finish = pos}, nil, true)
632 return newpos or pos
635 local function delete_nonsplicing(range, pos, delete_maybe_yank)
636 local action = {kill = true, wrap = false, splice = false, func = delete_maybe_yank}
637 range = extend_overlap(range)
638 local ndeleted, spliced, opening, _, refmt = pick_out(range, pos, action)
639 local backwards = pos == range.finish
640 if opening then
641 if spliced then
642 return pos - ndeleted
643 else
644 if ndeleted == 0 then
645 local closing = parser.opposite[opening] -- XXX: why don't I use the pick_out return value?
646 if closing == "\n" then
647 local sexp, parent, n = walker.sexp_at(range)
648 if pos == sexp.start + #sexp.d and backwards then
649 return _join_or_splice(parent, n, range, pos)
650 elseif pos == sexp.finish then
651 local r = {start = pos + #closing, finish = pos + #closing}
652 return _join_or_splice(parent, n, r, pos)
656 return backwards and (range.finish - ndeleted) or range.start
658 else
659 local newpos = backwards and range.start + (ndeleted <= 0 and ndeleted or 0) or (range.finish - ndeleted)
660 if refmt then
661 local r = {start = newpos, finish = newpos}
662 return refmt_at(big_enough_parent(range.start, range.finish - ndeleted), r, true)
664 return newpos
668 local function insert_pair(range, delimiter, auto_square)
669 local indices, nodes = walker.sexp_path(range)
670 local sexp = nodes[#nodes]
671 local right_after_prefix = sexp and sexp.p and range.start == sexp.start + #sexp.p
672 -- XXX: here I assume that # is a valid prefix for the dialect
673 local mb_closing = (delimiter == "|") and right_after_prefix and parser.opposite[sexp.p .. delimiter]
674 local closing = mb_closing or parser.opposite[delimiter]
675 local squarewords = fmt.squarewords
676 if delimiter == '"' and sexp and sexp.is_string
677 and range.start > sexp.start
678 and range.finish <= sexp.finish then
679 delimiter, closing = '\\'..delimiter, '\\'..closing
680 elseif squarewords and not right_after_prefix
681 and (auto_square or squarewords.not_optional)
682 and not fmt:adjust_bracket_p(indices, nodes, range) then
683 delimiter, closing = "[", "]"
685 write(range.finish, closing)
686 write(range.start, delimiter)
687 if right_after_prefix or parser.tree.is_parsed(range.start) then
688 parser.tree.rewind(right_after_prefix and sexp.start or range.start)
690 return range.start + #delimiter
693 local function make_wrap(opening)
694 return parser.opposite[opening] and function(range, pos, auto_square)
695 local newpos
696 local function _wrap(r)
697 if r.finish <= r.start then return end
698 newpos = insert_pair(r, opening, auto_square)
700 local action = {kill = false, wrap = false, splice = false, func = _wrap}
701 pick_out(range, pos, action)
702 -- XXX: here I assume that #| is the only multi-char delimitier,
703 -- and that it means block comment
704 local bump = newpos and #opening == 1 and (newpos - range.start) or 0
705 local rangeng = {start = range.start + bump, finish = range.finish + bump}
706 local _, parentng = walker.sexp_at(range) -- TODO: range.finish is wrong. use sexp_path and big_enough_parent
707 return refmt_at(parentng, rangeng)
711 local function convolute_lists(range)
712 local path, nodes = walker.sexp_path(range)
713 local parent = nodes[#nodes - 1]
714 local grandparent = nodes[#nodes - 2]
715 if not grandparent or not grandparent.is_list then return end
716 local gprange = { start = grandparent.start, finish = grandparent.finish + 1 }
717 local pprefix = parent.p
718 local prefix_len = pprefix and #pprefix or 0
719 insert_pair(gprange, parent.d)
720 local rangeng = { start = range.start + #parent.d, finish = range.finish + #parent.d }
721 path, nodes = walker.sexp_path(rangeng)
722 local sexp = nodes[#nodes]
723 parent = nodes[#nodes - 1]
724 grandparent = nodes[#nodes - 2]
725 delete(parent.finish, #parser.opposite[parent.d])
726 local head = parent.text:sub(prefix_len + #parent.d + 1, (sexp and sexp.start or rangeng.start) - parent.start)
727 delete(parent.start, (sexp and sexp.start or rangeng.start) - parent.start)
728 write(grandparent.start, head)
729 if pprefix then
730 write(gprange.start, pprefix)
731 gprange.finish = gprange.finish - #pprefix
733 parser.tree.rewind(gprange.start)
734 return refmt_at(gprange, {start = range.start, finish = range.start}) or range.start
737 local function newline(parent, range)
738 local line_comment = parent.is_line_comment
739 -- do not autoextend margin comments:
740 if line_comment and range.start < parent.finish + (parent.indent and 1 or 0) then
741 local newpos = split_anylist(range)
742 if newpos then
743 return newpos
746 if not parent.is_list and not line_comment then
747 -- if parent[#parent + 1] is nil, we are at EOF
748 if not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
749 parser.tree.rewind(parent.start or range.start)
751 local newpos = range.start
752 write(newpos, "\n")
753 return newpos + 1
755 if not parent.is_list then
756 local _
757 _, parent = walker.sexp_at(parent, true)
759 local last_nonblank_in_list = not parent.is_empty and parent[#parent].finish - (line_comment and 1 or 0)
760 local nxt = parent.after(range.start, startof)
761 local last_on_line = range.start == eol_at(range.start)
762 local margin = nxt and not nxt.indent and nxt.is_line_comment and nxt.finish
763 local after_last = last_nonblank_in_list and range.start > last_nonblank_in_list
764 local in_indent = nxt and nxt.indent and range.start <= nxt.start and range.start >= (nxt.start - nxt.indent)
765 local placeholder = "asdf"
766 local newpos = margin or range.start
767 if not parent.is_empty then
768 if in_indent then
769 write(newpos, (placeholder.."\n"))
770 else
771 write(newpos, "\n"..((after_last or last_on_line or margin) and placeholder or ""))
774 parser.tree.rewind(parent.start or newpos)
775 -- move the cursor onto the placeholder, so refmt_at can restore the position:
776 local r = {start = newpos, finish = newpos}
777 newpos = walker.start_after(r) or newpos
778 local rangeng = {start = newpos, finish = newpos}
779 newpos = refmt_at(parent, rangeng)
780 local _, parentng = walker.sexp_at({start = newpos, finish = newpos}, true)
781 if after_last then
782 local autoindent = parentng[#parentng].indent
783 write(parentng.finish, "\n"..string.rep(" ", autoindent or 0))
785 if after_last or last_on_line or margin or in_indent then
786 delete(newpos, #placeholder)
788 parser.tree.rewind(parentng.start or newpos)
789 return newpos
792 local function close_and_newline(range, closing)
793 local opening = closing and parser.opposite[closing]
794 local parent = walker.list_at(range, opening)
795 if parent then
796 local has_eol = parent.is_line_comment
797 local r = {start = parent.finish, finish = parent.finish}
798 local newpos = refmt_at(parent, r)
799 r = {start = newpos + (has_eol and 0 or 1), finish = newpos + (has_eol and 0 or 1)}
800 local list, parentng = walker.sexp_at(r)
801 newpos = list and list.finish + 1 or r.start
802 newpos = newline(parentng, {start = newpos, finish = newpos})
803 if not newpos then
804 newpos = r.finish + 1
805 write(newpos, "\n")
806 parser.tree.rewind(parentng.start or list.start)
808 return newpos
809 elseif closing == "\n" then
810 local eol = eol_at(range.start)
811 local _, parentng = walker.sexp_at({start = eol, finish = eol})
812 local newpos = newline(parentng, {start = eol, finish = eol})
813 return newpos
815 return range.start
818 local function join_line(_, pos)
819 local eol = eol_at(pos)
820 local r = {start = eol, finish = eol + 1}
821 return delete_nonsplicing(r, r.start, delete)
824 local block_comment_start
825 for o in pairs(parser.opposite) do
826 if #o > 1 then
827 block_comment_start = o
828 break
832 return {
833 delete_splicing = delete_splicing,
834 delete_nonsplicing = delete_nonsplicing,
835 refmt_at = refmt_at,
836 pick_out = pick_out,
837 raise_sexp = raise_sexp,
838 slurp_sexp = slurp_sexp,
839 barf_sexp = barf_sexp,
840 splice_anylist = splice_anylist,
841 wrap_round = make_wrap"(",
842 wrap_square = make_wrap"[",
843 wrap_curly = make_wrap"{",
844 wrap_doublequote = make_wrap'"',
845 wrap_comment = make_wrap(block_comment_start),
846 insert_pair = insert_pair,
847 newline = newline,
848 join_line = join_line,
849 close_and_newline = close_and_newline,
850 cycle_wrap = cycle_wrap,
851 split_anylist = split_anylist,
852 join_anylists = join_anylists,
853 convolute_lists = convolute_lists,
854 transpose_sexps = transpose_sexps,
855 transpose_words = transpose_words,
856 transpose_chars = transpose_chars,
860 return M