_always_ refmt when the operator element changes
[lisp-parkour.git] / edit.lua
blob7282b4195dfb30aa7092fb1b5dbb3c05566d1058
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, eol_at)
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 op_adj = 0
24 local pstart = parent.start + (parent.p and #parent.p or 0) + #parent.d
25 if parent.is_empty then
26 normalize_spacing(pstart, parent.finish - pstart, deltas)
27 return deltas
28 end
29 local last_distinguished = fmt:last_distinguished(parent)
30 for i, s in ipairs(parent) do
31 local delta
32 adj = s.indent and 0 or adj
33 if i == 2 and last_distinguished == 1 and not s.is_list and parent[1].text == "let" then
34 -- s is the name of a named let. account for it:
35 last_distinguished = last_distinguished + 1
36 end
37 local prev = parent[i - 1]
38 local has_eol = s.is_comment and parser.opposite[s.d] == "\n"
39 if not s.indent and not (prev and (s.d == "|" or prev.d == "|")) and not has_eol then
40 -- remove/add leading spaces
41 local off = prev and prev.finish + 1 or pstart
42 adj = adj + normalize_spacing(off, s.start - off - (prev and 1 or 0), deltas)
43 end
44 local nxt = parent[i + 1]
45 local is_last = not nxt
46 if (is_last and not keep_electric_space) or nxt and nxt.indent then
47 -- clean up trailing spaces
48 local off = is_last and parent.finish - (has_eol and i < #parent and 1 or 0) or nxt.start - nxt.indent - 1
49 local finish = s.finish + (has_eol and i < #parent and 0 or 1)
50 normalize_spacing(finish, off - finish, deltas)
51 end
52 if s.indent then
53 delta = base_indent - s.indent
54 if last_distinguished and i > 1 then
55 delta = delta + 1
56 if i - 1 <= last_distinguished then
57 delta = delta + 2
58 end
59 elseif i == 1 then
60 -- remove leading space, if any
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 > 2 and not parent[2].indent then
68 delta = base_indent + parent[1].finish + 2 - pstart - s.indent - op_adj
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 if i == 1 then
78 op_adj = adj
79 end
80 if s.is_list then
81 local nearest_indented = s.indent and s or parent.find_before(s, function(t) return t.indent end)
82 local parent_column = nearest_indented and
83 nearest_indented.indent + (s.start + (s.p and #s.p or 0) + #s.d) - nearest_indented.start
84 or base_indent + (s.start + (s.p and #s.p or 0) + #s.d) - pstart
85 refmt_list(s, parent_column - adj, padj + adj, deltas, keep_electric_space)
86 end
87 end
88 return deltas
89 end
91 local function refmt_at(scope, range, keep_electric_space)
92 if not range or not scope or scope.is_root then return end
93 local parent = walker.sexp_at(scope, true)
94 if not (parent and parent.is_list) then return end
95 local _, parent_at, m = walker.sexp_at(range, true)
96 if not m then
97 _, m = parent_at.after(range.start, startof)
98 if not m then
99 m = #parent_at
102 local base = parent_at[m] or parent_at
103 local path = walker.sexp_path(base)
104 local after_start, at_pfinish
105 if parent_at[m] then
106 local s = parent_at[#parent_at]
107 local has_eol = s.is_comment and parser.opposite[s.d] == "\n"
108 if range.start >= s.finish + 1 + (has_eol and 1 or 0) then
109 at_pfinish = true
110 else
111 after_start = range.start < base.start and 0 or range.start - base.start
113 else
114 after_start = (parent_at.p and #parent_at.p or 0) + #parent_at.d
116 local indented_parent = parent.indent and parent or parent.is_list and walker.indented_before(parent)
117 local parent_column = indented_parent and (indented_parent.indent + parent.start - indented_parent.start +
118 (parent.p and #parent.p or 0) + #parent.d) or
120 local deltas = refmt_list(parent, parent_column, 0, {}, keep_electric_space)
121 table.sort(deltas, function(d1, d2) return d1[1] > d2[1] end)
122 for _, pair in ipairs(deltas) do
123 local offset, delta = unpack(pair)
124 if delta > 0 then
125 write(offset, string.rep(" ", delta))
126 elseif delta < 0 then
127 delete(offset + delta, -delta)
130 parser.tree.rewind(parent.start or 0)
131 if path then
132 local sexp, parentng = walker.goto_path(path)
133 if sexp then
134 if at_pfinish then
135 return parentng.finish
136 else
137 return sexp.start + after_start
141 return range.start
144 local function splice(pos, sexps, skip, backwards, action)
145 local spliced
146 local sexp = walker.sexp_at(skip, true)
147 local start = sexp.start + (sexp.p and #sexp.p or 0)
148 local is_line_comment = sexp.is_comment and parser.opposite[sexp.d] == "\n"
149 -- XXX: don't splice empty line comments _yet_; see _join_or_splice
150 local tosplice = action.splice or action.wrap or not is_line_comment and sexp.is_empty
151 local opening = (sexp.p or '')..sexp.d
152 local closing = parser.opposite[sexp.d]
153 local real_start = tosplice and sexp.start or start + #sexp.d
154 local splice_closing = closing ~= "\n" or sexp.is_empty
155 local real_finish = sexp.finish + 1 - (tosplice and splice_closing and 0 or #closing)
156 local first = backwards and
157 {start = real_start, finish = math.max(pos, start + #sexp.d)} or
158 {start = real_start, finish = sexp.is_empty and pos or start + #sexp.d}
159 local second = backwards and
160 {start = sexp.is_empty and pos or sexp.finish + 1 - #closing, finish = real_finish} or
161 {start = math.min(pos, sexp.finish + 1 - #closing), finish = real_finish}
162 action.func(second)
163 action.func(first)
164 spliced = tosplice
165 if action.kill then
166 local ndeleted = first.finish - first.start + second.finish - second.start
167 if ndeleted > 0 then
168 sexps.rewind(sexp.start)
171 return first.finish - first.start, spliced, opening, closing
174 -- If you try to delete some part of indentation, this function joins the current line with
175 -- the previous one, unless the latter is a line comment.
176 -- it does this by
177 -- a) extending or shrinking the range that is to be deleted
178 -- b) returning a truthy value to trick pick_out to glide the cursor, but refmt to restore the deleted spaces
179 -- c) both a) and b)
180 local function delete_indentation(range, pos)
181 local node, parent = walker.sexp_at(range)
182 if node or not parent.is_list then return end
183 local prev = parent.before(pos, finishof)
184 local nxt = parent.after(pos, startof)
185 if prev and prev.finish >= range.start or nxt and nxt.start < range.finish then return end
186 local backwards = pos == range.finish
187 local adj = 0
188 local empty_line_after_comment = prev and prev.is_comment and parser.opposite[prev.d] == "\n"
189 and (not nxt or nxt.start > eol_at(pos))
190 if nxt and nxt.indent then
191 if backwards and prev then
192 if not prev.d then
193 range.finish = range.start
194 adj = 1
196 range.start = prev.finish + (empty_line_after_comment and 0 or 1)
198 return adj
200 if not nxt and prev then
201 range.start = prev.finish + 1
202 range.finish = parent.finish
203 elseif nxt and nxt.start == range.finish and (not prev or prev.finish + 1 == range.start) then
204 if prev and prev.d or nxt and nxt.d then
205 if backwards then
206 range.finish = range.start
207 else
208 range.start = range.finish
211 return adj
215 local function pick_out(range, pos, action)
216 local sexps = parser.tree
217 local skips = sexps.unbalanced_delimiters(range)
218 -- handle splice and kill-splice of forms and strings:
219 if #skips == 1 then
220 local sexp = walker.sexp_at(skips[1], true)
221 local backward_splice = skips[1].opening and pos >= sexp.start + (sexp.p and #sexp.p or 0) + #sexp.d
222 and range.start >= sexp.start
223 local forward_splice = skips[1].closing and pos <= sexp.finish + 1 - #parser.opposite[sexp.d]
224 and range.finish <= sexp.finish + 1
225 if backward_splice or forward_splice then
226 return splice(backward_splice and range.finish or range.start, sexps, sexp, backward_splice, action)
229 local node, parent = walker.sexp_at({start = range.finish, finish = range.finish})
230 local drop_eol = node and node.finish + 1 == range.finish and node.is_comment and parser.opposite[node.d] == "\n"
231 local _, par = walker.sexp_at({start = range.start, finish = range.start})
232 local operator_changed = par[1] and par[1].finish >= range.start
233 local refmt = #skips == 0 and delete_indentation(range, pos) or operator_changed and 0
234 table.sort(skips, function(a, b) return a.start < b.start end)
235 table.insert(skips, {start = range.finish - (drop_eol and 1 or 0)})
236 table.insert(skips, 1, {finish = range.start})
237 local ndeleted = drop_eol and 1 or 0
238 for i = #skips - 1, 1, -1 do
239 local region = {start = skips[i].finish, finish = skips[i + 1].start}
240 if skips[i].closing and skips[i + 1].opening then
241 -- leave out some of the space between adjacent lists
242 local _, rparent = walker.sexp_at(region)
243 local nxt = rparent.after(region.start, startof)
244 region.start = nxt and nxt.start or region.start
246 if action then
247 action.func(region)
248 ndeleted = ndeleted + (region.finish - region.start)
251 -- if parent[#parent + 1] is nil, we are at EOF
252 if ndeleted > 0 and (not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1]) then
253 sexps.rewind(range.start)
255 return ndeleted - (refmt or 0), nil, nil, nil, refmt
258 local function raise_sexp(range, pos)
259 local sexp, parent = walker.sexp_at(range, true)
260 if sexp and parent and parent.is_list then
261 delete(sexp.finish + 1, parent.finish - sexp.finish)
262 delete(parent.start, sexp.start - parent.start)
263 parser.tree.rewind(parent.start)
264 range.start = parent.start + pos - sexp.start
265 range.finish = range.start
266 local _, nodes = walker.sexp_path(range)
267 local grandparent = nodes[#nodes - 2]
268 return grandparent and refmt_at(grandparent, range) or range.start
272 local function slurp_sexp(range, forward)
273 local _, parent = walker.sexp_at(range, true)
274 local seeker = forward and walker.finish_after or walker.start_before
275 if not parent or not parent.is_list then return range.start end
276 local r = {start = parent.start, finish = parent.finish + 1}
277 local newpos = seeker(r, is_comment)
278 if not newpos then return range.start end
279 local opening = (parent.p or '')..parent.d
280 local closing = parser.opposite[parent.d]
281 local delimiter = forward and closing or opening
282 if forward then
283 write(newpos, delimiter)
285 delete(forward and parent.finish or parent.start, #delimiter)
286 if not forward then
287 write(newpos, delimiter)
289 parser.tree.rewind(math.min(parent.start, newpos))
290 local _, parentng = walker.sexp_at(range, true)
291 return refmt_at(parentng, range)
294 local function barf_sexp(range, forward)
295 local _, parent, m = walker.sexp_at(range, true)
296 local seeker = forward and walker.finish_before or walker.start_after
297 -- TODO: barfing out of strings requires calling the parser on them
298 if not parent or not parent.is_list then return range.start end
299 local opening = (parent.p or '')..parent.d
300 local pstart = parent.start + #opening
301 local r = {start = forward and parent.finish - 1 or pstart, finish = forward and parent.finish or pstart + 1}
302 local newpos = seeker(r, is_comment) or forward and pstart or parent.finish
303 if not newpos then return range.start end
304 local closing = parser.opposite[parent.d]
305 local delimiter = forward and closing or opening
306 if not forward then
307 write(newpos, delimiter)
309 delete(forward and parent.finish or parent.start, #delimiter)
310 if forward then
311 write(newpos, delimiter)
313 parser.tree.rewind(math.min(parent.start, newpos))
314 local barfed_cursor_backward = m == 1 and not forward
315 local barfed_cursor_forward = m == #parent and forward
316 range.start = range.start + #delimiter * (barfed_cursor_backward and -1 or barfed_cursor_forward and 1 or 0)
317 local barfed_cursor = barfed_cursor_forward or barfed_cursor_backward
318 local _, nodes = walker.sexp_path(range)
319 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
320 local scope = barfed_cursor and parentng and not parentng.is_root and parentng or grandparent
321 return refmt_at(scope, range) or range.start
324 local function splice_sexp(range, _, no_refmt)
325 local _, parent = walker.sexp_at(range)
326 if not parent or not parent.d then return end
327 local opening = (parent.p or '')..parent.d
328 local closing = parser.opposite[parent.d]
329 local finish = parent.finish + 1 - #closing
330 if closing ~= "\n" then
331 delete(finish, #closing)
333 -- TODO: (un)escape special characters, if necessary
334 delete(parent.start, parent.is_empty and (finish - parent.start) or #opening)
335 parser.tree.rewind(parent.start)
336 range.start = range.start - #opening
337 range.finish = range.start
338 local _, parentng = walker.sexp_at(range, true)
339 return not no_refmt and refmt_at(parentng, range) or range.start
342 local function rewrap(parent, kind)
343 local pstart = parent.start + #((parent.p or '')..parent.d) - 1
344 delete(parent.finish, 1)
345 write(parent.finish, parser.opposite[kind])
346 delete(pstart, #parent.d)
347 write(pstart, kind)
348 parser.tree.rewind(parent.start)
351 local function cycle_wrap(range, pos)
352 local _, parent = walker.sexp_at(range)
353 if not parent or not parent.is_list then return end
354 local next_kind = {["("] = "[", ["["] = "{", ["{"] = "("}
355 rewrap(parent, next_kind[parent.d])
356 return pos
359 local function split_sexp(range)
360 local _, parent = walker.sexp_at(range)
361 if not (parent and parent.d) then return end
362 local new_finish, new_start
363 if parent.is_list then
364 local prev = parent.before(range.start, finishof, is_comment)
365 new_finish = prev and prev.finish + 1
366 -- XXX: do not skip comments here, so they end up in the second list
367 -- and are not separated from their target expression:
368 local nxt = new_finish and parent.after(new_finish, startof)
369 new_start = nxt and nxt.start
370 else
371 new_start = range.start
372 new_finish = range.start
374 if not (new_start and new_finish) then return end
375 local opening = (parent.p or '')..parent.d
376 local closing = parser.opposite[parent.d]
377 write(new_start, opening)
378 local sep = parser.opposite[parent.d] == "\n" and "" -- line comments already have a separator
379 or new_finish == new_start and " " -- only add a separator if there was none before
380 or ""
381 write(new_finish, closing..sep)
382 parser.tree.rewind(parent.start)
383 range.start = new_start + (parent.is_list and 0 or #opening + #closing)
384 range.finish = range.start
385 local _, nodes = walker.sexp_path(range)
386 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
387 local scope = parentng and not parentng.is_root and parentng or grandparent
388 return refmt_at(scope, range) or range.start
391 local function join_sexps(range)
392 local node, parent = walker.sexp_at(range, true)
393 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
394 local second = first ~= node and node or parent.after(range.start, startof)
395 if not (first and second and first.d and first.indent and second.indent and
396 (first.d == second.d or
397 -- join line comments even when their delimiters differ slightly
398 -- (different number of semicolons, existence/lack of a space after them)
399 parser.opposite[first.d] == parser.opposite[second.d])) then
400 return
402 local opening = (second.p or '')..second.d
403 local closing = parser.opposite[first.d]
404 local pos
405 if not first.is_list then
406 pos = first.finish + 1 - #closing
407 delete(pos, second.start + #opening - pos)
408 else
409 delete(second.start, #opening)
410 delete(first.finish, #closing)
411 pos = second.start - #closing
413 parser.tree.rewind(first.start)
414 range.start = pos
415 range.finish = range.start
416 local _, nodes = walker.sexp_path(range)
417 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
418 local scope = parentng and not parentng.is_root and parentng or grandparent
419 return refmt_at(scope, range) or range.start
422 local function delete_splicing(range, pos, splicing, delete_and_yank)
423 local action = {kill = true, wrap = splicing, splice = splicing, func = delete_and_yank}
424 local sexp, parent, n = walker.sexp_at(range, true)
425 local ndeleted, spliced = pick_out(range, pos, action)
426 local closing = spliced and parser.opposite[sexp.d]
427 local inner_list_len = spliced and sexp.finish - sexp.start + 1 - #sexp.d - #closing
428 local range_len = range.finish - range.start
429 local backwards = pos == range.finish
430 local whole_object = sexp and
431 (sexp.start == range.start and sexp.finish + 1 == range.finish
432 or spliced and (inner_list_len <= (backwards and range_len + ndeleted or range_len)))
433 local in_head_atom = sexp and not sexp.d and n == 1 and #parent > 1
434 local in_whitespace = not sexp
435 if whole_object or in_whitespace or in_head_atom then
436 local cur = whole_object and sexp.start or range.start
437 -- if parent[#parent + 1] is nil, we are at EOF
438 if not parent.is_root or parent.is_parsed(cur) or parent[#parent + 1] then
439 parser.tree.rewind(cur)
441 local r = {start = cur, finish = cur}
442 local _, parentng = walker.sexp_at(r, true)
443 local newpos = refmt_at(parentng, r)
444 return newpos or cur
446 return spliced and (backwards and sexp.start or range.start - ndeleted)
447 or not backwards and ndeleted <= 0 and range.finish - ndeleted
448 or range.start
451 local function transpose(range, first, second)
452 if not (first and second) then return end
453 local copy1 = first.text
454 local copy2 = second.text
455 delete(second.start, second.finish + 1 - second.start)
456 write(second.start, copy1)
457 delete(first.start, first.finish + 1 - first.start)
458 write(first.start, copy2)
459 parser.tree.rewind(first.start)
460 range.start = second.finish + 1
461 range.finish = range.start
462 local _, parentng = walker.sexp_at(range, true)
463 return refmt_at(parentng, range) or range.start
466 local function transpose_sexps(range)
467 local node, parent = walker.sexp_at(range, true)
468 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
469 local second = first ~= node and node or parent.after(range.start, startof)
470 return transpose(range, first, second)
473 local function transpose_words(range)
474 local _, first = walker.start_float_before(range)
475 local _, second = walker.finish_float_after(range)
476 if first and second and first.start == second.start then
477 _, first = walker.start_float_before(first)
479 return transpose(range, first, second)
482 local function transpose_chars(range)
483 local node, parent, i = walker.sexp_at(range)
484 local nxt = i and parent[i + 1]
485 local pstart = not parent.is_root and parent.start + (parent.p and #parent.p or 0) + #parent.d
486 local pfinish = not parent.is_root and parent.finish
487 local npref = node and node.p and node.start + #node.p
488 -- only allow transposing while inside atoms/words and prefixes
489 if node and ((npref and (range.start <= npref and range.start > node.start) or
490 node.d and range.start > node.finish + 1) or not node.d and (not pstart or range.start > pstart)) then
491 local start = range.start -
492 ((range.start == pfinish or range.start == npref or
493 range.start == node.finish + 1 and (parent.is_list or parent.is_root) and (not nxt or nxt.indent)) and 1 or 0)
494 local str_start = start - node.start + 1
495 local char = node.text:sub(str_start, str_start)
496 delete(start, 1)
497 write(start - 1, #char > 0 and char or " ")
498 parser.tree.rewind(start)
499 local in_head_atom = i == 1 and #parent > 1
500 return parent.is_list and in_head_atom
501 and refmt_at(parent, {start = start + 1, finish = start + 1})
502 or start + 1
506 local function _join_or_splice(parent, n, range, pos)
507 local sexp = parent[n]
508 local nxt = parent[n + 1]
509 local is_last = not nxt or parser.opposite[nxt.d] ~= "\n"
510 local newpos = not (is_last and sexp.is_empty) and join_sexps(range)
511 if not newpos and sexp.is_empty then
512 newpos = splice_sexp({start = pos, finish = pos}, nil, true)
514 return newpos or pos
517 local function delete_nonsplicing(range, pos, delete_maybe_yank)
518 local action = {kill = true, wrap = false, splice = false, func = delete_maybe_yank}
519 local ndeleted, spliced, opening, _, refmt = pick_out(range, pos, action)
520 local backwards = pos == range.finish
521 if opening then
522 if spliced then
523 return pos - ndeleted
524 else
525 if ndeleted == 0 then
526 local closing = parser.opposite[opening]
527 if closing == "\n" then
528 local sexp, parent, n = walker.sexp_at(range)
529 if pos == sexp.start + #sexp.d and backwards then
530 return _join_or_splice(parent, n, range, pos)
531 elseif pos == sexp.finish then
532 local r = {start = pos + #closing, finish = pos + #closing}
533 return _join_or_splice(parent, n, r, pos)
537 return backwards and (range.finish - ndeleted) or range.start
539 else
540 local newpos = backwards and range.start or (range.finish - ndeleted)
541 local r = {start = newpos, finish = newpos}
542 if refmt then
543 -- since the range can cross list boundaries, find which of the lists at both ends
544 -- of the range is larger, and reformat it:
545 local _, p1, p2
546 _, p1 = walker.sexp_at({start = range.start, finish = range.start}, true)
547 if p1.is_root or not (p1.start < range.start and p1.finish > range.finish - ndeleted) then
548 _, p2 = walker.sexp_at({start = range.finish - ndeleted, finish = range.finish - ndeleted}, true)
550 return refmt_at(p2 and not p2.is_root and p2 or p1, r, true)
552 return newpos
556 local function insert_pair(range, delimiter, shiftless, auto_square)
557 local indices, nodes = walker.sexp_path(range)
558 local sexp = nodes[#nodes]
559 local right_after_prefix = sexp and sexp.p and range.start == sexp.start + #sexp.p
560 -- XXX: here I assume that # is a valid prefix for the dialect
561 local mb_closing = (delimiter == "|") and right_after_prefix and parser.opposite[sexp.p .. delimiter]
562 local closing = mb_closing or parser.opposite[delimiter]
563 local squarewords = fmt.squarewords
564 if squarewords and not right_after_prefix
565 and not (closing == "]" and shiftless and not auto_square)
566 and (auto_square or squarewords.not_optional)
567 and not fmt:adjust_bracket_p(indices, nodes, range) then
568 delimiter, closing = "[", "]"
570 write(range.finish, closing)
571 write(range.start, delimiter)
572 if right_after_prefix or parser.tree.is_parsed(range.start) then
573 parser.tree.rewind(right_after_prefix and sexp.start or range.start)
575 return range.start + #delimiter
578 local function make_wrap(kind)
579 return function(range, pos, auto_square)
580 local opening = kind
581 local function _wrap(r)
582 insert_pair(r, opening, false, auto_square)
584 local action = {kill = false, wrap = false, splice = false, func = _wrap}
585 pick_out(range, pos, action)
586 parser.tree.rewind(range.start)
587 local _, parentng = walker.sexp_at(range, true)
588 range.start = range.start + #opening
589 return refmt_at(parentng, range) or range.start
593 local function newline(parent, range)
594 local line_comment = parent.d and parser.opposite[parent.d] == "\n"
595 -- do not autoextend margin comments:
596 if line_comment and range.start < parent.finish + (parent.indent and 1 or 0) then
597 local newpos = split_sexp(range)
598 if newpos then
599 return newpos
602 if not parent.is_list and not line_comment then
603 -- if parent[#parent + 1] is nil, we are at EOF
604 if not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
605 parser.tree.rewind(parent.start or range.start)
607 return
609 if not parent.is_list then
610 local _
611 _, parent = walker.sexp_at(parent, true)
613 local last_nonblank_in_list = not parent.is_empty and parent[#parent].finish - (line_comment and 1 or 0)
614 local nxt = parent.after(range.start, startof)
615 local last_on_line = not nxt or nxt and nxt.indent and nxt.start > range.start
616 local margin = nxt and not nxt.indent and nxt.is_comment and parser.opposite[nxt.d] == "\n" and nxt.finish
617 local after_last = last_nonblank_in_list and range.start > last_nonblank_in_list
618 local placeholder = "asdf"
619 local newpos = margin or range.start
620 if not parent.is_empty then
621 write(newpos, "\n"..((after_last or last_on_line or margin) and placeholder or ""))
623 parser.tree.rewind(parent.start or newpos)
624 -- move the cursor onto the placeholder, so refmt_at can restore the position:
625 local r = {start = newpos, finish = newpos}
626 newpos = walker.start_after(r) or newpos
627 local rangeng = {start = newpos, finish = newpos}
628 newpos = refmt_at(parent, rangeng) or rangeng.start
629 if after_last then
630 local _, parentng = walker.sexp_at({start = newpos, finish = newpos}, true)
631 local autoindent = parentng[#parentng].indent
632 write(parentng.finish, "\n"..string.rep(" ", autoindent or 0))
634 if after_last or last_on_line or margin then
635 delete(newpos, #placeholder)
637 parser.tree.rewind(newpos)
638 return newpos
641 local function close_and_newline(range)
642 local _, parent = walker.sexp_at(range)
643 if parent and not parent.is_root then
644 local r = {start = parent.finish, finish = parent.finish}
645 local newpos = refmt_at(parent, r)
646 r = {start = (newpos or r.finish) + 1, finish = (newpos or r.finish) + 1}
647 local list, parentng = walker.sexp_at(r)
648 newpos = newline(parentng, {start = list.finish + 1, finish = list.finish + 1})
649 if not newpos then
650 newpos = list.finish + 1
651 write(newpos, "\n")
653 return newpos
655 return range.start
658 local function meta_doublequote(range, pos, auto_square)
659 local escaped = walker.escaped_at(range)
660 if not escaped then
661 local _, parent = walker.sexp_at(range)
662 local newpos = insert_pair(range, '"', false, auto_square)
663 return refmt_at(parent, {start = newpos, finish = newpos}) or newpos
664 elseif escaped.is_string then
665 return close_and_newline(range)
667 return pos
670 return {
671 delete_splicing = delete_splicing,
672 delete_nonsplicing = delete_nonsplicing,
673 refmt_at = refmt_at,
674 pick_out = pick_out,
675 raise_sexp = raise_sexp,
676 slurp_sexp = slurp_sexp,
677 barf_sexp = barf_sexp,
678 splice_sexp = splice_sexp,
679 wrap_round = make_wrap("("),
680 meta_doublequote = meta_doublequote,
681 insert_pair = insert_pair,
682 newline = newline,
683 close_and_newline = close_and_newline,
684 cycle_wrap = cycle_wrap,
685 split_sexp = split_sexp,
686 join_sexps = join_sexps,
687 transpose_sexps = transpose_sexps,
688 transpose_words = transpose_words,
689 transpose_chars = transpose_chars,
693 return M