scheme: remove #\ as a prefix
[lisp-parkour.git] / edit.lua
blobe2d7bfa07a7f8db1df8aeea17699002ec8aa0060
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 is_line_comment(node) return node.is_comment and parser.opposite[node.d] == "\n" end
14 local function normalize_spacing(start, delta, list)
15 if delta > 0 then
16 table.insert(list, {start + delta, -delta})
17 elseif delta < 0 then
18 table.insert(list, {start, -delta})
19 end
20 return delta
21 end
23 local function leading_space(s, deltas, adj, parent, i, pstart, has_eol)
24 local prev = parent[i - 1]
25 if not s.indent and not (prev and (s.d == "|" or prev.d == "|")) and not has_eol then
26 local off = prev and prev.finish + 1 or pstart
27 adj = adj + normalize_spacing(off, s.start - off - (prev and 1 or 0), deltas)
28 end
29 return adj
30 end
32 local function trailing_space(s, deltas, adj, parent, i, has_eol, keep_electric_space)
33 local nxt = parent[i + 1]
34 local is_last = not nxt
35 if (is_last and not keep_electric_space) or nxt and nxt.indent then
36 local off = is_last and parent.finish - (has_eol and i < #parent and 1 or 0) or nxt.start - nxt.indent - 1
37 local finish = s.finish + (has_eol and i < #parent and 0 or 1)
38 normalize_spacing(finish, off - finish, deltas)
39 end
40 return adj
41 end
43 local function indentation(s, deltas, adj, parent, i, pstart, base_indent, last_distinguished, first_argument,
44 op_adj, indent_adj)
45 if s.indent then
46 local delta = base_indent - s.indent
47 local firstarg_delta = base_indent + parent[1].finish + 2 - pstart - s.indent - op_adj - (indent_adj or 0)
48 if last_distinguished and i > 1 then
49 delta = delta + 1
50 if i - 1 <= last_distinguished then
51 if parent[first_argument + 1].indent then
52 delta = delta + 2
53 else
54 delta = firstarg_delta
55 end
56 elseif last_distinguished < 0
57 and i > first_argument + 1 and not parent[first_argument + 1].indent then
58 delta = firstarg_delta
59 end
60 elseif i == 1 then
61 -- remove leading space, if any
62 delta = -(s.start - pstart)
63 elseif -- align further arguments below the second one
64 parent.d == "(" -- [] and {} contain data, so no arguments
65 and not parent[1].is_string -- a string can't have arguments
66 --and not parent[1].is_list -- wrong, GNU Emacs compatible behaviour
67 then
68 if i > first_argument + 1 and not parent[first_argument + 1].indent then
69 delta = firstarg_delta
70 end
71 if delta < 0 then delta = math.max(-s.indent, delta) end
72 end
73 if delta ~= 0 then
74 table.insert(deltas, {s.start, delta})
75 end
76 adj = adj - delta
77 end
78 return adj
79 end
81 local function refmt_list(parent, base_indent, padj, deltas, keep_electric_space)
82 local adj = 0
83 local op_adj = 0
84 local indent_adj = 0
85 local pstart = parent.start + (parent.p and #parent.p or 0) + #parent.d
86 if parent.is_empty then
87 normalize_spacing(pstart, parent.finish - pstart, deltas)
88 return deltas
89 end
90 local last_distinguished = fmt:last_distinguished(parent)
91 local first_argument = 1
92 if #parent > 2 and parent[1].is_list and #parent[1] > 1 then
93 local op = parent[1]
94 local nearest_indented = op[#op].indent and op[#op] or walker.indented_before(op[#op])
95 indent_adj = nearest_indented.start - pstart - nearest_indented.indent + 1
96 end
97 for i, s in ipairs(parent) do
98 adj = s.indent and 0 or adj
99 if last_distinguished then
100 -- do not let early comments influence the indentation of real expressions:
101 if i <= last_distinguished and s.is_comment then
102 last_distinguished = last_distinguished + 1
104 -- s is the name of a named let. account for it:
105 if i == last_distinguished + 1 and not s.is_list and parent[1].text == "let" then
106 last_distinguished = last_distinguished + 1
108 -- do not let early comments influence the indentation of real expressions:
109 elseif i <= first_argument + 1 and s.is_comment then
110 first_argument = first_argument + 1
112 local has_eol = is_line_comment(s)
113 adj = leading_space(s, deltas, adj, parent, i, pstart, has_eol)
114 adj = trailing_space(s, deltas, adj, parent, i, has_eol, keep_electric_space)
115 adj = indentation(s, deltas, adj, parent, i, pstart, base_indent, last_distinguished, first_argument,
116 op_adj, indent_adj)
117 if i == 1 then
118 op_adj = adj
120 if s.is_list then
121 local nearest_indented = s.indent and s or walker.indented_before(s)
122 local parent_column = nearest_indented and
123 nearest_indented.indent + (s.start + (s.p and #s.p or 0) + #s.d) - nearest_indented.start
124 or base_indent + (s.start + (s.p and #s.p or 0) + #s.d) - pstart
125 refmt_list(s, parent_column - adj, padj + adj, deltas, keep_electric_space)
128 return deltas
131 local function path_from(range)
132 local _, parent_at, m = walker.sexp_at(range, true)
133 local path
134 if parent_at[m] then
135 path = walker.sexp_path(parent_at[m])
136 local base = parent_at[m]
137 if range.finish == base.finish + 1 then
138 path.after_finish = 0 -- lists can end up with a different length
139 else
140 path.after_start = range.start - base.start
142 else
143 local s = parent_at[#parent_at]
144 if not s or range.start >= s.finish + 1 + (is_line_comment(s) and 1 or 0) then
145 path = walker.sexp_path(parent_at)
146 path.at_pfinish = true
147 else
148 local prev, n = parent_at.before(range.start, finishof)
149 if prev then
150 path = walker.sexp_path(prev)
151 local nxt = parent_at[n + 1]
152 path.after_finish = nxt and not nxt.indent and 1 or range.finish - prev.finish - 1
153 else
154 path = walker.sexp_path(parent_at[1])
155 path.after_start = 0
159 return path
162 local function pos_from(path, range)
163 local sexp, parentng, n = walker.goto_path(path)
164 if sexp then
165 if path.after_finish then
166 local nxt = n and parentng[n + 1]
167 local max = nxt and nxt.start - sexp.finish - 1
168 return sexp.finish + 1 - (range.finish - range.start) +
169 (max and math.min(path.after_finish, max) or path.after_finish)
170 elseif path.at_pfinish then
171 return sexp.finish
172 else
173 return sexp.start + path.after_start
178 local function refmt_at(scope, range, keep_electric_space)
179 if not range or not scope or not scope.start or not scope.finish or
180 (scope.finish - scope.start < 3) or scope.is_root then return range.start end
181 local parent = walker.sexp_at(scope, true)
182 if not (parent and parent.is_list) then return range.start end
183 local path = path_from(range)
184 local indented_parent = parent.indent and parent or parent.is_list and walker.indented_before(parent)
185 local parent_column = indented_parent and (indented_parent.indent + parent.start - indented_parent.start +
186 (parent.p and #parent.p or 0) + #parent.d) or
188 local deltas = refmt_list(parent, parent_column, 0, {}, keep_electric_space)
189 table.sort(deltas, function(d1, d2) return d1[1] > d2[1] end)
190 for _, pair in ipairs(deltas) do
191 local offset, delta = unpack(pair)
192 if delta > 0 then
193 write(offset, string.rep(" ", delta))
194 elseif delta < 0 then
195 delete(offset + delta, -delta)
198 parser.tree.rewind(parent.start or 0)
199 return path and pos_from(path, range) or range.start
202 local function splice(pos, sexps, skip, backwards, action)
203 local spliced
204 local sexp = walker.sexp_at(skip, true)
205 local start = sexp.start + (sexp.p and #sexp.p or 0)
206 -- XXX: don't splice empty line comments _yet_; see _join_or_splice
207 local tosplice = action.splice or action.wrap or not is_line_comment(sexp) and sexp.is_empty
208 local opening = (sexp.p or '')..sexp.d
209 local closing = parser.opposite[sexp.d]
210 local real_start = tosplice and sexp.start or start + #sexp.d
211 local splice_closing = closing ~= "\n" or sexp.is_empty
212 local real_finish = sexp.finish + 1 - (tosplice and splice_closing and 0 or #closing)
213 local first = backwards and
214 {start = real_start, finish = math.max(pos, start + #sexp.d)} or
215 {start = real_start, finish = sexp.is_empty and pos or start + #sexp.d}
216 local second = backwards and
217 {start = sexp.is_empty and pos or sexp.finish + 1 - #closing, finish = real_finish} or
218 {start = math.min(pos, sexp.finish + 1 - #closing), finish = real_finish}
219 action.func(second)
220 action.func(first)
221 spliced = tosplice
222 if action.kill then
223 local ndeleted = first.finish - first.start + second.finish - second.start
224 if ndeleted > 0 then
225 sexps.rewind(sexp.start)
228 return first.finish - first.start, spliced, opening, closing
231 -- If you try to delete some part of indentation, this function joins the current line with
232 -- the previous one, unless the latter is a line comment.
233 -- it does this by
234 -- a) extending or shrinking the range that is to be deleted
235 -- b) returning a truthy value to trick pick_out to glide the cursor, but refmt to restore the deleted spaces
236 -- c) both a) and b)
237 local function delete_indentation(range, pos)
238 local node, parent = walker.sexp_at(range)
239 if node or not parent.is_list then return end
240 local prev = parent.before(pos, finishof)
241 local nxt = parent.after(pos, startof)
242 if prev and prev.finish >= range.start or nxt and nxt.start < range.finish then return end
243 local backwards = pos == range.finish
244 local adj = 0
245 local eol = eol_at(pos)
246 local on_empty_line = not nxt or eol and nxt.start > eol
247 local empty_line_after_comment = prev and is_line_comment(prev)
248 and on_empty_line
249 if nxt and nxt.indent then
250 if backwards and prev then
251 range.start = prev.finish + (empty_line_after_comment and 0 or 1)
252 if not prev.d then
253 range.finish = range.start + 1
254 adj = (on_empty_line and 0 or 1)
257 return adj
259 if not nxt and prev then
260 range.start = prev.finish + 1
261 range.finish = parent.finish
262 elseif nxt and nxt.start == range.finish and (not prev or prev.finish + 1 == range.start) then
263 if prev and prev.d or nxt and nxt.d then
264 -- don't delete spaces near delimiters, just slide the cursor:
265 if backwards then
266 range.finish = range.start
267 else
268 range.start = range.finish
271 return adj
275 local function big_enough_parent(pos1, pos2)
276 -- since the pos1-pos2 range can cross list boundaries, find which list contains both pos1 and pos2
277 local _, p1, p2
278 _, p1 = walker.sexp_at({start = pos1, finish = pos1}, true)
279 if p1.is_root or not (p1.start < pos1 and p1.finish > pos2) then
280 _, p2 = walker.sexp_at({start = pos2, finish = pos2}, true)
282 return p2 and not p2.is_root and p2 or p1
285 local function extend_overlap(range)
286 local rnode = walker.sexp_at({start = range.finish, finish = range.finish})
287 if rnode and rnode.p and rnode.p:find";"
288 and range.finish > rnode.start and range.finish < rnode.start + #rnode.p then
289 return {start = range.start, finish = rnode.start + #rnode.p}
291 return range
294 local function pick_out(range, pos, action)
295 local ndeleted = 0
296 if range.start == range.finish then return ndeleted end
297 local sexps = parser.tree
298 local skips = sexps.unbalanced_delimiters(range)
299 -- handle splice and kill-splice of forms and strings:
300 if #skips == 1 then
301 local sexp = walker.sexp_at(skips[1], true)
302 local backward_splice = skips[1].opening and pos >= sexp.start + (sexp.p and #sexp.p or 0) + #sexp.d
303 and range.start >= sexp.start
304 local forward_splice = skips[1].closing and pos <= sexp.finish + 1 - #parser.opposite[sexp.d]
305 and range.finish <= sexp.finish + 1
306 if backward_splice or forward_splice then
307 return splice(backward_splice and range.finish or range.start, sexps, sexp, backward_splice, action)
310 local node, parent = walker.sexp_at({start = range.finish, finish = range.finish})
311 -- if the range ends with a line comment, don't delete its closing newline:
312 local drop_eol = action.kill and node and
313 node.finish + 1 == range.finish and is_line_comment(node)
314 local par = big_enough_parent(range.start, range.finish)
315 local operator_changed = par[1] and par[1].finish >= range.start
316 local refmt = #skips == 0 and delete_indentation(range, pos) or operator_changed and 0
317 table.sort(skips, function(a, b) return a.start < b.start end)
318 table.insert(skips, {start = range.finish - (drop_eol and 1 or 0)})
319 table.insert(skips, 1, {finish = range.start})
320 ndeleted = ndeleted + (drop_eol and 1 or 0)
321 for i = #skips - 1, 1, -1 do
322 local region = {start = skips[i].finish, finish = skips[i + 1].start}
323 if skips[i].closing and skips[i + 1].opening then
324 -- leave out some of the space between adjacent lists
325 local _, rparent = walker.sexp_at(region)
326 local nxt = rparent.after(region.start, startof)
327 region.start = nxt and nxt.start or region.start
329 if action then
330 action.func(region)
331 ndeleted = ndeleted + (region.finish - region.start)
334 -- if parent[#parent + 1] is nil, we are at EOF
335 if ndeleted > 0 and (not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1]) then
336 sexps.rewind(range.start)
338 return ndeleted - (refmt or 0), nil, nil, nil, refmt
341 local function raise_sexp(range, pos)
342 local sexp, parent = walker.sexp_at(range, true)
343 if sexp and parent and parent.is_list then
344 delete(sexp.finish + 1, parent.finish - sexp.finish)
345 delete(parent.start, sexp.start - parent.start)
346 parser.tree.rewind(parent.start)
347 range.start = parent.start + pos - sexp.start
348 range.finish = range.start
349 local _, nodes = walker.sexp_path(range)
350 local grandparent = nodes[#nodes - 2]
351 return grandparent and refmt_at(grandparent, range) or range.start
355 local function slurp_sexp(range, forward)
356 local _, parent = walker.sexp_at(range, true)
357 local seeker = forward and walker.finish_after or walker.start_before
358 if not parent or not parent.is_list then return range.start end
359 local r = {start = parent.start, finish = parent.finish + 1}
360 local newpos = seeker(r, is_comment)
361 if not newpos then return range.start end
362 local opening = (parent.p or '')..parent.d
363 local closing = parser.opposite[parent.d]
364 local delimiter = forward and closing or opening
365 if forward then
366 write(newpos, delimiter)
368 delete(forward and parent.finish or parent.start, #delimiter)
369 if not forward then
370 write(newpos, delimiter)
372 parser.tree.rewind(math.min(parent.start, newpos))
373 return refmt_at(big_enough_parent(newpos, range.start), range)
376 local function barf_sexp(range, forward)
377 local _, parent, m = walker.sexp_at(range, true)
378 local seeker = forward and walker.finish_before or walker.start_after
379 -- TODO: barfing out of strings requires calling the parser on them
380 if not parent or not parent.is_list then return range.start end
381 local opening = (parent.p or '')..parent.d
382 local pstart = parent.start + #opening
383 local r = {start = forward and parent.finish - 1 or pstart, finish = forward and parent.finish or pstart + 1}
384 local newpos = seeker(r, is_comment) or forward and pstart or parent.finish
385 if not newpos then return range.start end
386 local closing = parser.opposite[parent.d]
387 local delimiter = forward and closing or opening
388 if not forward then
389 write(newpos, delimiter)
391 delete(forward and parent.finish or parent.start, #delimiter)
392 if forward then
393 write(newpos, delimiter)
395 parser.tree.rewind(math.min(parent.start, newpos))
396 local barfed_cursor_backward = m == 1 and not forward
397 local barfed_cursor_forward = m == #parent and forward
398 range.start = range.start + #delimiter * (barfed_cursor_backward and -1 or barfed_cursor_forward and 1 or 0)
399 return refmt_at(big_enough_parent(newpos, parent.finish + 1), range)
402 local function splice_sexp(range, _, no_refmt)
403 local _, parent = walker.sexp_at(range)
404 if not parent or not parent.d then return end
405 local opening = (parent.p or '')..parent.d
406 local closing = parser.opposite[parent.d]
407 local finish = parent.finish + 1 - #closing
408 if closing ~= "\n" then
409 delete(finish, #closing)
411 -- TODO: (un)escape special characters, if necessary
412 delete(parent.start, parent.is_empty and (finish - parent.start) or #opening)
413 parser.tree.rewind(parent.start)
414 range.start = range.start - #opening
415 range.finish = range.start
416 local _, parentng = walker.sexp_at(range, true)
417 return not no_refmt and refmt_at(parentng, range) or range.start
420 local function rewrap(parent, kind)
421 local pstart = parent.start + #((parent.p or '')..parent.d) - 1
422 delete(parent.finish, 1)
423 write(parent.finish, parser.opposite[kind])
424 delete(pstart, #parent.d)
425 write(pstart, kind)
426 parser.tree.rewind(parent.start)
429 local function cycle_wrap(range, pos)
430 local _, parent = walker.sexp_at(range)
431 if not parent or not parent.is_list then return end
432 local next_kind = {["("] = "[", ["["] = "{", ["{"] = "("}
433 rewrap(parent, next_kind[parent.d])
434 return pos
437 local function split_sexp(range)
438 local _, parent = walker.sexp_at(range)
439 if not (parent and parent.d) then return end
440 local new_finish, new_start
441 if parent.is_list then
442 local prev = parent.before(range.start, finishof, is_comment)
443 new_finish = prev and prev.finish + 1
444 -- XXX: do not skip comments here, so they end up in the second list
445 -- and are not separated from their target expression:
446 local nxt = new_finish and parent.after(new_finish, startof)
447 new_start = nxt and nxt.start
448 else
449 new_start = range.start
450 new_finish = range.start
452 if not (new_start and new_finish) then return end
453 local opening = (parent.p or '')..parent.d
454 local closing = parser.opposite[parent.d]
455 write(new_start, opening)
456 local sep = is_line_comment(parent) and "" -- line comments already have a separator
457 or new_finish == new_start and " " -- only add a separator if there was none before
458 or ""
459 write(new_finish, closing..sep)
460 parser.tree.rewind(parent.start)
461 range.start = new_start + (parent.is_list and 0 or #opening + #closing)
462 range.finish = range.start
463 local _, nodes = walker.sexp_path(range)
464 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
465 local scope = parentng and not parentng.is_root and parentng or grandparent
466 return refmt_at(scope, range)
469 local function join_sexps(range)
470 local node, parent = walker.sexp_at(range, true)
471 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
472 local second = first ~= node and node or parent.after(range.start, startof)
473 if not (first and second and first.d and
474 -- don't join line comments to margin comments:
475 (not is_line_comment(first) or first.indent and second.indent) and
476 (first.d == second.d or
477 -- join line comments even when their delimiters differ slightly
478 -- (different number of semicolons, existence/lack of a space after them)
479 parser.opposite[first.d] == parser.opposite[second.d])) then
480 return
482 local opening = (second.p or '')..second.d
483 local closing = parser.opposite[first.d]
484 local pos
485 if not first.is_list then
486 pos = first.finish + 1 - #closing
487 delete(pos, second.start + #opening - pos)
488 else
489 delete(second.start, #opening)
490 delete(first.finish, #closing)
491 pos = second.start - #closing
493 parser.tree.rewind(first.start)
494 range.start = pos
495 range.finish = range.start
496 local _, nodes = walker.sexp_path(range)
497 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
498 local scope = parentng and not parentng.is_root and parentng or grandparent
499 return refmt_at(scope, range)
502 local function delete_splicing(range, pos, splicing, delete_and_yank)
503 local action = {kill = true, wrap = splicing, splice = splicing, func = delete_and_yank}
504 local sexp, parent, n = walker.sexp_at(range, true)
505 range = extend_overlap(range)
506 local ndeleted, spliced = pick_out(range, pos, action)
507 local closing = spliced and parser.opposite[sexp.d]
508 local inner_list_len = spliced and sexp.finish - sexp.start + 1 - #sexp.d - #closing
509 local range_len = range.finish - range.start
510 local backwards = pos == range.finish
511 local whole_object = sexp and
512 (sexp.start == range.start and sexp.finish + 1 == range.finish
513 or spliced and (inner_list_len <= (backwards and range_len + ndeleted or range_len)))
514 local in_head_atom = sexp and not sexp.d and n == 1 and #parent > 1
515 local in_whitespace = not sexp
516 if whole_object or in_whitespace or in_head_atom then
517 local cur = whole_object and sexp.start or range.start
518 -- if parent[#parent + 1] is nil, we are at EOF
519 if not parent.is_root or parent.is_parsed(cur) or parent[#parent + 1] then
520 parser.tree.rewind(cur)
522 local r = {start = cur, finish = cur}
523 local _, parentng = walker.sexp_at(r, true)
524 return refmt_at(parentng, r)
526 return spliced and (backwards and sexp.start or range.start - ndeleted)
527 or not backwards and ndeleted <= 0 and range.finish - ndeleted
528 or range.start
531 local function transpose(range, first, second)
532 if not (first and second) then return end
533 local copy1 = first.text
534 local copy2 = second.text
535 delete(second.start, second.finish + 1 - second.start)
536 write(second.start, copy1)
537 delete(first.start, first.finish + 1 - first.start)
538 write(first.start, copy2)
539 parser.tree.rewind(first.start)
540 range.start = second.finish + 1
541 range.finish = range.start
542 return refmt_at(big_enough_parent(first.start, second.finish), range)
545 local function transpose_sexps(range)
546 local node, parent = walker.sexp_at(range, true)
547 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
548 local second = first ~= node and node or parent.after(range.start, startof)
549 return transpose(range, first, second)
552 local function transpose_words(range)
553 local _, first = walker.start_float_before(range)
554 local _, second = walker.finish_float_after(range)
555 if first and second and first.start == second.start then
556 _, first = walker.start_float_before(first)
558 return transpose(range, first, second)
561 local function transpose_chars(range)
562 local node, parent, i = walker.sexp_at(range)
563 local nxt = i and parent[i + 1]
564 local pstart = not parent.is_root and parent.start + (parent.p and #parent.p or 0) + #parent.d
565 local pfinish = not parent.is_root and parent.finish
566 local npref = node and node.p and node.start + #node.p
567 -- only allow transposing while inside atoms/words and prefixes
568 if node and ((npref and (range.start <= npref and range.start > node.start) or
569 node.d and range.start > node.finish + 1) or not node.d and (not pstart or range.start > pstart)) then
570 local start = range.start -
571 ((range.start == pfinish or range.start == npref or
572 range.start == node.finish + 1 and (parent.is_list or parent.is_root) and (not nxt or nxt.indent)) and 1 or 0)
573 local str_start = start - node.start + 1
574 local char = node.text:sub(str_start, str_start)
575 delete(start, 1)
576 write(start - 1, #char > 0 and char or " ")
577 parser.tree.rewind(start)
578 local in_head_atom = i == 1 and #parent > 1
579 return parent.is_list and in_head_atom and refmt_at(parent, {start = start + 1, finish = start + 1}) or start + 1
583 local function _join_or_splice(parent, n, range, pos)
584 local sexp = parent[n]
585 local nxt = parent[n + 1]
586 local is_last = not nxt or not is_line_comment(nxt)
587 local newpos = not (is_last and sexp.is_empty) and join_sexps(range)
588 if not newpos and sexp.is_empty then
589 newpos = splice_sexp({start = pos, finish = pos}, nil, true)
591 return newpos or pos
594 local function delete_nonsplicing(range, pos, delete_maybe_yank)
595 local action = {kill = true, wrap = false, splice = false, func = delete_maybe_yank}
596 range = extend_overlap(range)
597 local ndeleted, spliced, opening, _, refmt = pick_out(range, pos, action)
598 local backwards = pos == range.finish
599 if opening then
600 if spliced then
601 return pos - ndeleted
602 else
603 if ndeleted == 0 then
604 local closing = parser.opposite[opening] -- XXX: why don't I use the pick_out return value?
605 if closing == "\n" then
606 local sexp, parent, n = walker.sexp_at(range)
607 if pos == sexp.start + #sexp.d and backwards then
608 return _join_or_splice(parent, n, range, pos)
609 elseif pos == sexp.finish then
610 local r = {start = pos + #closing, finish = pos + #closing}
611 return _join_or_splice(parent, n, r, pos)
615 return backwards and (range.finish - ndeleted) or range.start
617 else
618 local newpos = backwards and range.start or (range.finish - ndeleted)
619 if refmt then
620 local r = {start = newpos, finish = newpos}
621 return refmt_at(big_enough_parent(range.start, range.finish - ndeleted), r, true)
623 return newpos
627 local function insert_pair(range, delimiter, shiftless, auto_square)
628 local indices, nodes = walker.sexp_path(range)
629 local sexp = nodes[#nodes]
630 local right_after_prefix = sexp and sexp.p and range.start == sexp.start + #sexp.p
631 -- XXX: here I assume that # is a valid prefix for the dialect
632 local mb_closing = (delimiter == "|") and right_after_prefix and parser.opposite[sexp.p .. delimiter]
633 local closing = mb_closing or parser.opposite[delimiter]
634 local squarewords = fmt.squarewords
635 if squarewords and not right_after_prefix
636 and not (closing == "]" and shiftless and not auto_square)
637 and (auto_square or squarewords.not_optional)
638 and not fmt:adjust_bracket_p(indices, nodes, range) then
639 delimiter, closing = "[", "]"
641 write(range.finish, closing)
642 write(range.start, delimiter)
643 if right_after_prefix or parser.tree.is_parsed(range.start) then
644 parser.tree.rewind(right_after_prefix and sexp.start or range.start)
646 return range.start + #delimiter
649 local function make_wrap(kind)
650 return function(range, pos, auto_square)
651 local opening = kind
652 local function _wrap(r)
653 if not (r.finish > r.start) then return end
654 insert_pair(r, opening, false, auto_square)
656 local action = {kill = false, wrap = false, splice = false, func = _wrap}
657 pick_out(range, pos, action)
658 range.start = range.start + #opening
659 return refmt_at(big_enough_parent(range.start - #opening, range.start), range)
663 local function newline(parent, range)
664 local line_comment = is_line_comment(parent)
665 -- do not autoextend margin comments:
666 if line_comment and range.start < parent.finish + (parent.indent and 1 or 0) then
667 local newpos = split_sexp(range)
668 if newpos then
669 return newpos
672 if not parent.is_list and not line_comment then
673 -- if parent[#parent + 1] is nil, we are at EOF
674 if not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
675 parser.tree.rewind(parent.start or range.start)
677 local newpos = range.start
678 write(newpos, "\n")
679 return newpos + 1
681 if not parent.is_list then
682 local _
683 _, parent = walker.sexp_at(parent, true)
685 local last_nonblank_in_list = not parent.is_empty and parent[#parent].finish - (line_comment and 1 or 0)
686 local nxt = parent.after(range.start, startof)
687 local last_on_line = not nxt or nxt and nxt.indent and nxt.start > range.start
688 local margin = nxt and not nxt.indent and is_line_comment(nxt) and nxt.finish
689 local after_last = last_nonblank_in_list and range.start > last_nonblank_in_list
690 local in_indent = nxt and nxt.indent and range.start <= nxt.start and range.start >= (nxt.start - nxt.indent)
691 local placeholder = "asdf"
692 local newpos = margin or range.start
693 if not parent.is_empty then
694 if in_indent then
695 write(newpos, (placeholder.."\n"))
696 else
697 write(newpos, "\n"..((after_last or last_on_line or margin) and placeholder or ""))
700 parser.tree.rewind(parent.start or newpos)
701 -- move the cursor onto the placeholder, so refmt_at can restore the position:
702 local r = {start = newpos, finish = newpos}
703 newpos = walker.start_after(r) or newpos
704 local rangeng = {start = newpos, finish = newpos}
705 newpos = refmt_at(parent, rangeng)
706 local _, parentng = walker.sexp_at({start = newpos, finish = newpos}, true)
707 if after_last then
708 local autoindent = parentng[#parentng].indent
709 write(parentng.finish, "\n"..string.rep(" ", autoindent or 0))
711 if after_last or last_on_line or margin or in_indent then
712 delete(newpos, #placeholder)
714 parser.tree.rewind(parentng.start or newpos)
715 return newpos
718 local function close_and_newline(range, closing)
719 local opening = closing and parser.opposite[closing]
720 local parent = walker.quasilist_at(range, opening)
721 if parent then
722 local has_eol = is_line_comment(parent)
723 local r = {start = parent.finish, finish = parent.finish}
724 local newpos = refmt_at(parent, r)
725 r = {start = newpos + (has_eol and 0 or 1), finish = newpos + (has_eol and 0 or 1)}
726 local list, parentng = walker.sexp_at(r)
727 newpos = list and list.finish + 1 or r.start
728 newpos = newline(parentng, {start = newpos, finish = newpos})
729 if not newpos then
730 newpos = r.finish + 1
731 write(newpos, "\n")
732 parser.tree.rewind(parentng.start or list.start)
734 return newpos
735 elseif closing == "\n" then
736 local eol = eol_at(range.start)
737 local _, parentng = walker.sexp_at({start = eol, finish = eol})
738 local newpos = newline(parentng, {start = eol, finish = eol})
739 return newpos
741 return range.start
744 local function join_line(_, pos)
745 local eol = eol_at(pos)
746 local r = {start = eol, finish = eol + 1}
747 return delete_nonsplicing(r, r.start, delete)
750 local wrap_doublequote = make_wrap'"'
752 local function meta_doublequote(range, pos, auto_square)
753 local escaped = walker.escaped_at(range)
754 if not escaped then
755 return wrap_doublequote(range, pos, auto_square)
756 elseif escaped.is_string then
757 return close_and_newline(range, '"')
759 return pos
762 return {
763 delete_splicing = delete_splicing,
764 delete_nonsplicing = delete_nonsplicing,
765 refmt_at = refmt_at,
766 pick_out = pick_out,
767 raise_sexp = raise_sexp,
768 slurp_sexp = slurp_sexp,
769 barf_sexp = barf_sexp,
770 splice_sexp = splice_sexp,
771 wrap_round = make_wrap"(",
772 wrap_square = make_wrap"[",
773 wrap_curly = make_wrap"{",
774 meta_doublequote = meta_doublequote,
775 insert_pair = insert_pair,
776 newline = newline,
777 join_line = join_line,
778 close_and_newline = close_and_newline,
779 cycle_wrap = cycle_wrap,
780 split_sexp = split_sexp,
781 join_sexps = join_sexps,
782 transpose_sexps = transpose_sexps,
783 transpose_words = transpose_words,
784 transpose_chars = transpose_chars,
788 return M