clean up
[lisp-parkour.git] / edit.lua
blob70887e16f809dcd011178d385c3c25c05890baf2
1 local M = {}
3 -- XXX: in Lua 5.2 unpack() was moved into table
4 local unpack = unpack or table.unpack
6 local swap = {["["] = "(", ["{"] = "[", ["("] = "{"}
8 local function startof(node) return node.start end
9 local function finishof(node) return node.finish end
10 local function is_comment(node) return node.is_comment end
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 leading_space(s, deltas, adj, parent, i, pstart, has_eol)
22 local prev = parent[i - 1]
23 if not s.indent and not (prev and (s.d == "|" or prev.d == "|")) and not has_eol then
24 local off = prev and prev.finish + 1 or pstart
25 adj = adj + normalize_spacing(off, s.start - off - (prev and 1 or 0), deltas)
26 end
27 return adj
28 end
30 local function trailing_space(s, deltas, adj, parent, i, has_eol, keep_electric_space)
31 local nxt = parent[i + 1]
32 local is_last = not nxt
33 if (is_last and not keep_electric_space) or nxt and nxt.indent then
34 local off = is_last and parent.finish - (has_eol and i < #parent and 1 or 0) or nxt.start - nxt.indent - 1
35 local finish = s.finish + (has_eol and i < #parent and 0 or 1)
36 normalize_spacing(finish, off - finish, deltas)
37 end
38 return adj
39 end
41 local function indentation(s, deltas, adj, parent, i, pstart, base_indent, last_distinguished, first_argument,
42 op_adj, indent_adj)
43 if s.indent then
44 local delta = base_indent - s.indent
45 local firstarg_delta = base_indent + parent[1].finish + 2 - pstart - s.indent - op_adj - (indent_adj or 0)
46 if last_distinguished and i > 1 then
47 delta = delta + 1
48 if i - 1 <= last_distinguished then
49 if parent[first_argument + 1].indent then
50 delta = delta + 2
51 else
52 delta = firstarg_delta
53 end
54 elseif last_distinguished < 0
55 and i > first_argument + 1 and not parent[first_argument + 1].indent then
56 delta = firstarg_delta
57 end
58 elseif i == 1 then
59 -- remove leading space
60 delta = -(s.start - pstart)
61 elseif -- align further arguments below the second one
62 parent.d == "(" -- [] and {} contain data, so no arguments
63 and not parent[1].is_string -- a string can't have arguments
64 --and not parent[1].is_list -- wrong, GNU Emacs compatible behaviour
65 then
66 if i > first_argument + 1 and not parent[first_argument + 1].indent then
67 delta = firstarg_delta
68 end
69 if delta < 0 then delta = math.max(-s.indent, delta) end
70 end
71 if delta ~= 0 then
72 table.insert(deltas, {s.start, delta})
73 end
74 adj = adj - delta
75 end
76 return adj
77 end
79 local function is_lone_prefix(node)
80 return node and node.p and (#node.p == node.finish - node.start + 1)
81 end
83 function M.new(parser, walker, fmt, write, delete, eol_at)
85 local function refmt_list(parent, base_indent, padj, deltas, keep_electric_space)
86 local adj = 0
87 local op_adj = 0
88 local indent_adj = 0
89 local pstart = parent.start + (parent.p and #parent.p or 0) + #parent.d
90 if parent.is_empty then
91 normalize_spacing(pstart, parent.finish - pstart, deltas)
92 return deltas
93 end
94 local last_distinguished = fmt:last_distinguished(parent)
95 local first_argument = 1
96 if #parent > 2 and parent[1].is_list and #parent[1] > 1 then
97 local op = parent[1]
98 local nearest_indented = op[#op].indent and op[#op] or walker.indented_before(op[#op])
99 indent_adj = nearest_indented.start - pstart - nearest_indented.indent + 1
101 for i, s in ipairs(parent) do
102 adj = s.indent and 0 or adj
103 if last_distinguished then
104 -- do not let early comments influence the indentation of real expressions:
105 if i <= last_distinguished and s.is_comment then
106 last_distinguished = last_distinguished + 1
108 -- s is the name of a named let. account for it:
109 if i == last_distinguished + 1 and not s.is_list and parent[1].text == "let" then
110 last_distinguished = last_distinguished + 1
112 -- do not let early comments influence the indentation of real expressions:
113 elseif i <= first_argument + 1 and s.is_comment then
114 first_argument = first_argument + 1
116 local has_eol = s.is_line_comment
117 adj = leading_space(s, deltas, adj, parent, i, pstart, has_eol)
118 adj = trailing_space(s, deltas, adj, parent, i, has_eol, keep_electric_space)
119 adj = indentation(s, deltas, adj, parent, i, pstart, base_indent, last_distinguished, first_argument,
120 op_adj, indent_adj)
121 if i == 1 then
122 op_adj = adj
124 if s.is_list then
125 local nearest_indented = s.indent and s or walker.indented_before(s)
126 local parent_column = nearest_indented and
127 nearest_indented.indent + (s.start + (s.p and #s.p or 0) + #s.d) - nearest_indented.start
128 or base_indent + (s.start + (s.p and #s.p or 0) + #s.d) - pstart
129 refmt_list(s, parent_column - adj, padj + adj, deltas, keep_electric_space)
132 return deltas
135 local function path_from(range)
136 local _, parent_at, m = walker.sexp_at(range, true)
137 local path
138 if parent_at[m] then
139 path = walker.sexp_path(parent_at[m])
140 local base = parent_at[m]
141 if range.finish == base.finish + 1 then
142 path.after_finish = 0 -- lists can end up with a different length
143 else
144 path.after_start = range.start - base.start
146 else
147 local s = parent_at[#parent_at]
148 if not s or range.start >= s.finish + 1 + (s.is_line_comment and 1 or 0) then
149 path = walker.sexp_path(parent_at)
150 path.at_pfinish = true
151 else
152 local prev, n = parent_at.before(range.start, finishof)
153 if prev then
154 path = walker.sexp_path(prev)
155 local nxt = parent_at[n + 1]
156 path.after_finish = nxt and not nxt.indent and 1 or range.finish - prev.finish - 1
157 else
158 path = walker.sexp_path(parent_at[1])
159 path.after_start = 0
163 return path
166 local function pos_from(path, range)
167 local sexp, parentng, n = walker.goto_path(path)
168 if sexp then
169 if path.after_finish then
170 local nxt = n and parentng[n + 1]
171 local max = nxt and nxt.start - sexp.finish - 1
172 return sexp.finish + 1 - (range.finish - range.start) +
173 (max and math.min(path.after_finish, max) or path.after_finish)
174 elseif path.at_pfinish then
175 return sexp.finish
176 else
177 return sexp.start + path.after_start
182 local function refmt_at(scope, range, keep_electric_space)
183 if not range or not scope or scope.is_root or (scope.finish - scope.start < 2) then return range.start end
184 local parent = walker.sexp_at(scope, true)
185 if not (parent and parent.is_list) then return range.start end
186 local path = path_from(range)
187 local indented_parent = parent.indent and parent or parent.is_list and walker.indented_before(parent)
188 local parent_column = indented_parent and (indented_parent.indent + parent.start - indented_parent.start +
189 (parent.p and #parent.p or 0) + #parent.d) or
191 local deltas = refmt_list(parent, parent_column, 0, {}, keep_electric_space)
192 table.sort(deltas, function(d1, d2) return d1[1] > d2[1] end)
193 for _, pair in ipairs(deltas) do
194 local offset, delta = unpack(pair)
195 if delta > 0 then
196 write(offset, string.rep(" ", delta))
197 elseif delta < 0 then
198 delete(offset + delta, -delta)
201 parser.tree.rewind(parent.start or 0)
202 return path and pos_from(path, range) or range.start
205 local function splice(pos, sexps, skip, backwards, action)
206 local spliced
207 local sexp = walker.sexp_at(skip, true)
208 local start = sexp.start + (sexp.p and #sexp.p or 0)
209 -- XXX: don't splice empty line comments _yet_. See _join_or_splice
210 local tosplice = action.splice or action.wrap or not sexp.is_line_comment and sexp.is_empty
211 local opening = (sexp.p or "")..sexp.d
212 local closing = parser.opposite[sexp.d]
213 local real_start = tosplice and sexp.start or start + #sexp.d
214 local splice_closing = not sexp.is_line_comment or sexp.is_empty
215 local real_finish = sexp.finish + 1 - (tosplice and splice_closing and 0 or #closing)
216 local first = backwards and
217 {start = real_start, finish = math.max(pos, start + #sexp.d)} or
218 {start = real_start, finish = sexp.is_empty and pos or start + #sexp.d}
219 local second = backwards and
220 {start = sexp.is_empty and pos or sexp.finish + 1 - #closing, finish = real_finish} or
221 {start = math.min(pos, sexp.finish + 1 - #closing), finish = real_finish}
222 action.func(second)
223 action.func(first)
224 spliced = tosplice
225 if action.kill then
226 local ndeleted = first.finish - first.start + second.finish - second.start
227 if ndeleted > 0 then
228 sexps.rewind(sexp.start)
231 return first.finish - first.start, spliced, opening, closing
235 -- This function handles some whitespace-only deletion corner cases magically.
236 -- It does so by
237 -- a) extending or shrinking the range that is to be deleted
238 -- b) returning an integer to trick pick_out to glide the cursor, but calling refmt to restore the deleted spaces
239 -- c) both a) and b)
240 local function delete_whitespace_only(range, pos)
241 local node, parent = walker.sexp_at(range)
242 if node or not parent.is_list then return end
243 local prev = parent.before(pos, finishof)
244 local nxt = parent.after(pos, startof)
245 if prev and prev.finish >= range.start or nxt and nxt.start < range.finish then return end
246 local backwards = pos == range.finish
247 local adj = 0
248 local eol = eol_at(pos)
249 local on_empty_line = not nxt or eol and nxt.start > eol
250 local empty_line_after_comment = prev and prev.is_line_comment
251 and on_empty_line
252 if nxt and nxt.indent then
253 if backwards and prev then
254 -- join current line with the previous one, unless the latter is a line comment
255 range.start = prev.finish + (empty_line_after_comment and 0 or 1)
256 if not prev.d then
257 range.finish = range.start + 1
258 adj = (on_empty_line and 0 or 1)
260 else
261 adj = nxt.start - range.finish
263 return adj
265 if not nxt and prev then
266 -- clean up trailing whitespace (e.g. electric RET)
267 range.start = prev.finish + 1
268 range.finish = parent.finish
269 elseif nxt and nxt.start == range.finish and (not prev or prev.finish + 1 == range.start) then
270 if prev and prev.d or nxt and nxt.d and not is_lone_prefix(prev) then
271 -- don't delete spaces near delimiters, just slide the cursor:
272 if backwards then
273 range.finish = range.start
274 else
275 range.start = range.finish
278 return adj
282 local function big_enough_parent(pos1, pos2)
283 -- since the pos1-pos2 range can cross list boundaries, find which list contains both pos1 and pos2
284 local _, p1, p2
285 _, p1 = walker.sexp_at({start = pos1, finish = pos1}, true)
286 if p1.is_root or not (p1.start < pos1 and p1.finish > pos2) then
287 _, p2 = walker.sexp_at({start = pos2, finish = pos2}, true)
289 return p2 and not p2.is_root and p2 or p1
292 local function extend_overlap(range)
293 local rnode = walker.sexp_at({start = range.finish, finish = range.finish})
294 if rnode and rnode.p and rnode.p:find";"
295 and range.finish > rnode.start and range.finish < rnode.start + #rnode.p then
296 return {start = range.start, finish = rnode.start + #rnode.p}
298 return range
301 local function pick_out(range, pos, action)
302 local ndeleted = 0
303 if range.start == range.finish then return ndeleted end
304 local sexps = parser.tree
305 local skips = sexps.unbalanced_delimiters(range)
306 -- handle splice and kill-splice of forms and strings:
307 if #skips == 1 then
308 local sexp = walker.sexp_at(skips[1], true)
309 local backward_splice = skips[1].opening and pos >= sexp.start + (sexp.p and #sexp.p or 0) + #sexp.d
310 and range.start >= sexp.start
311 local forward_splice = skips[1].closing and pos <= sexp.finish + 1 - #parser.opposite[sexp.d]
312 and range.finish <= sexp.finish + 1
313 if backward_splice or forward_splice then
314 return splice(backward_splice and range.finish or range.start, sexps, sexp, backward_splice, action)
317 local node, parent = walker.sexp_at({start = range.finish, finish = range.finish})
318 -- if the range ends with a line comment, don't delete its closing newline:
319 local drop_eol = action.kill and node and
320 node.finish + 1 == range.finish and node.is_line_comment
321 local par = big_enough_parent(range.start, range.finish)
322 local operator_changed = par[1] and par[1].finish >= range.start
323 local refmt = #skips == 0 and delete_whitespace_only(range, pos) or operator_changed and 0
324 table.sort(skips, function(a, b) return a.start < b.start end)
325 table.insert(skips, {start = range.finish - (drop_eol and 1 or 0)})
326 table.insert(skips, 1, {finish = range.start})
327 ndeleted = ndeleted + (drop_eol and 1 or 0)
328 for i = #skips - 1, 1, -1 do
329 local region = {start = skips[i].finish, finish = skips[i + 1].start}
330 if skips[i].closing and skips[i + 1].opening then
331 -- leave out some of the space between adjacent lists
332 local _, rparent = walker.sexp_at(region)
333 local nxt = rparent.after(region.start, startof)
334 region.start = nxt and nxt.start or region.start
336 if action then
337 action.func(region)
338 ndeleted = ndeleted + (region.finish - region.start)
341 -- if parent[#parent + 1] is nil, we are at EOF
342 if ndeleted > 0 and (not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1]) then
343 sexps.rewind(range.start)
345 return ndeleted - (refmt or 0), nil, nil, nil, refmt
348 local function raise_sexp(range, pos)
349 local sexp, parent = walker.sexp_at(range, true)
350 if sexp and parent and parent.is_list then
351 delete(sexp.finish + 1, parent.finish - sexp.finish)
352 delete(parent.start, sexp.start - parent.start)
353 parser.tree.rewind(parent.start)
354 range.start = parent.start + pos - sexp.start
355 range.finish = range.start
356 local _, nodes = walker.sexp_path(range)
357 local grandparent = nodes[#nodes - 2]
358 return grandparent and refmt_at(grandparent, range) or range.start
362 local function slurp_sexp(range, forward)
363 local _, parent = walker.sexp_at(range, true)
364 local seeker = forward and walker.finish_after or walker.start_before
365 if not parent or not parent.is_list then return range.start end
366 local r = {start = parent.start, finish = parent.finish + 1}
367 local newpos = seeker(r, is_comment)
368 if not newpos then return range.start end
369 local opening = (parent.p or "")..parent.d
370 local closing = parser.opposite[parent.d]
371 local delimiter = forward and closing or opening
372 if forward then
373 write(newpos, delimiter)
375 delete(forward and parent.finish or parent.start, #delimiter)
376 if not forward then
377 write(newpos, delimiter)
379 parser.tree.rewind(math.min(parent.start, newpos))
380 return refmt_at(big_enough_parent(newpos, range.start), range)
383 local function barf_sexp(range, forward)
384 local _, parent = walker.sexp_at(range, true)
385 local seeker = forward and walker.finish_before or walker.start_after
386 -- TODO: barfing out of strings requires calling the parser on them
387 if not parent or not parent.is_list or parent.is_empty then return range.start end
388 local opening = (parent.p or "")..parent.d
389 local pstart = parent.start + #opening
390 local r = {start = forward and parent.finish - 1 or pstart, finish = forward and parent.finish or pstart + 1}
391 local newpos = seeker(r, is_comment) or forward and pstart or parent.finish
392 local closing = parser.opposite[parent.d]
393 local delimiter = forward and closing or opening
394 if not forward then
395 write(newpos, delimiter)
397 delete(forward and parent.finish or parent.start, #delimiter)
398 if forward then
399 write(newpos, delimiter)
401 parser.tree.rewind(math.min(parent.start, newpos))
402 local drag = forward and (newpos < range.finish) or not forward and (newpos > range.start)
403 local keep_inside = forward and #parent > 1 and range.finish > range.start and 1 or 0
404 newpos = drag and newpos - keep_inside or range.start
405 local rangeng = {start = newpos, finish = newpos}
406 return refmt_at(big_enough_parent(newpos, parent.finish + 1), rangeng)
409 local function splice_sexp(range, _, no_refmt)
410 local _, parent = walker.sexp_at(range)
411 if not parent or not parent.d then return end
412 local opening = (parent.p or "")..parent.d
413 local closing = parser.opposite[parent.d]
414 local finish = parent.finish + 1 - #closing
415 if not parent.is_line_comment then
416 delete(finish, #closing)
418 -- TODO: (un)escape special characters, if necessary
419 delete(parent.start, parent.is_empty and (finish - parent.start) or #opening)
420 parser.tree.rewind(parent.start)
421 range.start = range.start - #opening
422 range.finish = range.start
423 local _, parentng = walker.sexp_at(range, true)
424 return not no_refmt and refmt_at(parentng, range) or range.start
427 local function rewrap(parent, kind)
428 local pstart = parent.start + #((parent.p or "")..parent.d) - 1
429 delete(parent.finish, 1)
430 write(parent.finish, parser.opposite[kind])
431 delete(pstart, #parent.d)
432 write(pstart, kind)
433 parser.tree.rewind(parent.start)
436 local function cycle_wrap(range, pos)
437 local _, parent = walker.sexp_at(range)
438 if not parent or not parent.is_list then return end
439 local next_kind = {["("] = "[", ["["] = "{", ["{"] = "("}
440 rewrap(parent, next_kind[parent.d])
441 return pos
444 local function split_sexp(range)
445 local _, parent = walker.sexp_at(range)
446 if not (parent and parent.d) then return end
447 local new_finish, new_start
448 if parent.is_list then
449 local prev = parent.before(range.start, finishof, is_comment)
450 new_finish = prev and prev.finish + 1
451 -- XXX: do not skip comments here, so they end up in the second list
452 -- and are not separated from their target expression:
453 local nxt = new_finish and parent.after(new_finish, startof)
454 new_start = nxt and nxt.start
455 else
456 new_start = range.start
457 new_finish = range.start
459 if not (new_start and new_finish) then return end
460 local opening = (parent.p or "")..parent.d
461 local closing = parser.opposite[parent.d]
462 write(new_start, opening)
463 local sep = parent.is_line_comment and "" -- line comments already have a separator
464 or new_finish == new_start and " " -- only add a separator if there was none before
465 or ""
466 write(new_finish, closing..sep)
467 parser.tree.rewind(parent.start)
468 range.start = new_start + (parent.is_list and 0 or #opening + #closing)
469 range.finish = range.start
470 local _, nodes = walker.sexp_path(range)
471 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
472 local scope = parentng and not parentng.is_root and parentng or grandparent
473 return refmt_at(scope, range)
476 local function join_sexps(range)
477 local node, parent = walker.sexp_at(range, true)
478 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
479 local second = first ~= node and node or parent.after(range.start, startof)
480 if not (first and second and first.d and
481 -- don't join line comments to margin comments:
482 (not first.is_line_comment or first.indent and second.indent) and
483 (first.d == second.d or
484 -- join line comments even when their delimiters differ slightly
485 -- (different number of semicolons, existence/lack of a space after them)
486 parser.opposite[first.d] == parser.opposite[second.d])) then
487 return
489 local opening = (second.p or "")..second.d
490 local closing = parser.opposite[first.d]
491 local pos
492 if not first.is_list then
493 pos = first.finish + 1 - #closing
494 delete(pos, second.start + #opening - pos)
495 else
496 delete(second.start, #opening)
497 delete(first.finish, #closing)
498 pos = second.start - #closing
500 parser.tree.rewind(first.start)
501 range.start = pos
502 range.finish = range.start
503 local _, nodes = walker.sexp_path(range)
504 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
505 local scope = parentng and not parentng.is_root and parentng or grandparent
506 return refmt_at(scope, range)
509 local function delete_splicing(range, pos, splicing, delete_and_yank)
510 local action = {kill = true, wrap = splicing, splice = splicing, func = delete_and_yank}
511 local sexp, parent, n = walker.sexp_at(range, true)
512 range = extend_overlap(range)
513 local ndeleted, spliced = pick_out(range, pos, action)
514 local closing = spliced and parser.opposite[sexp.d]
515 local inner_list_len = spliced and sexp.finish - sexp.start + 1 - #sexp.d - #closing
516 local range_len = range.finish - range.start
517 local backwards = pos == range.finish
518 local whole_object = sexp and
519 (sexp.start == range.start and sexp.finish + 1 == range.finish
520 or spliced and (inner_list_len <= (backwards and range_len + ndeleted or range_len)))
521 local in_head_atom = sexp and not sexp.d and n == 1 and #parent > 1
522 local in_whitespace = not sexp
523 if whole_object or in_whitespace or in_head_atom then
524 local cur = whole_object and sexp.start or range.start
525 -- if parent[#parent + 1] is nil, we are at EOF
526 if not parent.is_root or parent.is_parsed(cur) or parent[#parent + 1] then
527 parser.tree.rewind(cur)
529 local r = {start = cur, finish = cur}
530 local _, parentng = walker.sexp_at(r, true)
531 return refmt_at(parentng, r)
533 return spliced and (backwards and sexp.start or range.start - ndeleted)
534 or not backwards and ndeleted <= 0 and range.finish - ndeleted
535 or backwards and ndeleted <= 0 and range.start + ndeleted
536 or range.start
539 local function transpose(range, first, second)
540 if not (first and second) then return end
541 local copy1 = first.text
542 local copy2 = second.text
543 delete(second.start, second.finish + 1 - second.start)
544 write(second.start, copy1)
545 delete(first.start, first.finish + 1 - first.start)
546 write(first.start, copy2)
547 parser.tree.rewind(first.start)
548 range.start = second.finish + 1
549 range.finish = range.start
550 return refmt_at(big_enough_parent(first.start, second.finish), range)
553 local function transpose_sexps(range)
554 local node, parent = walker.sexp_at(range, true)
555 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
556 local second = first ~= node and node or parent.after(range.start, startof)
557 return transpose(range, first, second)
560 local function transpose_words(range)
561 local _, first = walker.start_float_before(range)
562 local _, second = walker.finish_float_after(range)
563 if first and second and first.start == second.start then
564 _, first = walker.start_float_before(first)
566 return transpose(range, first, second)
569 local function transpose_chars(range)
570 local node, parent, i = walker.sexp_at(range)
571 local nxt = i and parent[i + 1]
572 local pstart = not parent.is_root and parent.start + (parent.p and #parent.p or 0) + #parent.d
573 local pfinish = not parent.is_root and parent.finish
574 local npref = node and node.p and node.start + #node.p
575 -- only allow transposing while inside atoms/words and prefixes
576 if node and ((npref and (range.start <= npref and range.start > node.start) or
577 node.d and range.start > node.finish + 1) or not node.d and (not pstart or range.start > pstart)) then
578 local start = range.start -
579 ((range.start == pfinish or range.start == npref or
580 range.start == node.finish + 1 and (parent.is_list or parent.is_root) and (not nxt or nxt.indent)) and 1 or 0)
581 local str_start = start - node.start + 1
582 local char = node.text:sub(str_start, str_start)
583 delete(start, 1)
584 write(start - 1, #char > 0 and char or " ")
585 parser.tree.rewind(start)
586 local in_head_atom = i == 1 and #parent > 1
587 return parent.is_list and in_head_atom and refmt_at(parent, {start = start + 1, finish = start + 1}) or start + 1
591 local function _join_or_splice(parent, n, range, pos)
592 local sexp = parent[n]
593 local nxt = parent[n + 1]
594 local is_last = not nxt or not nxt.is_line_comment
595 local newpos = not (is_last and sexp.is_empty) and join_sexps(range)
596 if not newpos and sexp.is_empty then
597 newpos = splice_sexp({start = pos, finish = pos}, nil, true)
599 return newpos or pos
602 local function delete_nonsplicing(range, pos, delete_maybe_yank)
603 local action = {kill = true, wrap = false, splice = false, func = delete_maybe_yank}
604 range = extend_overlap(range)
605 local ndeleted, spliced, opening, _, refmt = pick_out(range, pos, action)
606 local backwards = pos == range.finish
607 if opening then
608 if spliced then
609 return pos - ndeleted
610 else
611 if ndeleted == 0 then
612 local closing = parser.opposite[opening] -- XXX: why don't I use the pick_out return value?
613 if closing == "\n" then
614 local sexp, parent, n = walker.sexp_at(range)
615 if pos == sexp.start + #sexp.d and backwards then
616 return _join_or_splice(parent, n, range, pos)
617 elseif pos == sexp.finish then
618 local r = {start = pos + #closing, finish = pos + #closing}
619 return _join_or_splice(parent, n, r, pos)
623 return backwards and (range.finish - ndeleted) or range.start
625 else
626 local newpos = backwards and range.start + (ndeleted <= 0 and ndeleted or 0) or (range.finish - ndeleted)
627 if refmt then
628 local r = {start = newpos, finish = newpos}
629 return refmt_at(big_enough_parent(range.start, range.finish - ndeleted), r, true)
631 return newpos
635 local function insert_pair(range, delimiter, shiftless, auto_square)
636 local indices, nodes = walker.sexp_path(range)
637 local sexp = nodes[#nodes]
638 local right_after_prefix = sexp and sexp.p and range.start == sexp.start + #sexp.p
639 -- XXX: here I assume that # is a valid prefix for the dialect
640 local mb_closing = (delimiter == "|") and right_after_prefix and parser.opposite[sexp.p .. delimiter]
641 local closing = mb_closing or parser.opposite[delimiter]
642 local squarewords = fmt.squarewords
643 if squarewords and not right_after_prefix
644 and not (closing == "]" and shiftless and not auto_square)
645 and (auto_square or squarewords.not_optional)
646 and not fmt:adjust_bracket_p(indices, nodes, range) then
647 delimiter, closing = "[", "]"
649 write(range.finish, closing)
650 write(range.start, delimiter)
651 if right_after_prefix or parser.tree.is_parsed(range.start) then
652 parser.tree.rewind(right_after_prefix and sexp.start or range.start)
654 return range.start + #delimiter
657 local function make_wrap(kind)
658 return function(range, pos, shiftless, auto_square)
659 local opening = shiftless and swap[kind] or kind
660 local function _wrap(r)
661 if not (r.finish > r.start) then return end
662 insert_pair(r, opening, false, auto_square)
664 local action = {kill = false, wrap = false, splice = false, func = _wrap}
665 pick_out(range, pos, action)
666 local rangeng = {start = pos + #opening, finish = range.finish}
667 local _, parentng = walker.sexp_at(range) -- TODO: range.finish is wrong. use sexp_path and big_enough_parent
668 return refmt_at(parentng, rangeng)
672 local function newline(parent, range)
673 local line_comment = parent.is_line_comment
674 -- do not autoextend margin comments:
675 if line_comment and range.start < parent.finish + (parent.indent and 1 or 0) then
676 local newpos = split_sexp(range)
677 if newpos then
678 return newpos
681 if not parent.is_list and not line_comment then
682 -- if parent[#parent + 1] is nil, we are at EOF
683 if not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
684 parser.tree.rewind(parent.start or range.start)
686 local newpos = range.start
687 write(newpos, "\n")
688 return newpos + 1
690 if not parent.is_list then
691 local _
692 _, parent = walker.sexp_at(parent, true)
694 local last_nonblank_in_list = not parent.is_empty and parent[#parent].finish - (line_comment and 1 or 0)
695 local nxt = parent.after(range.start, startof)
696 local last_on_line = range.start == eol_at(range.start)
697 local margin = nxt and not nxt.indent and nxt.is_line_comment and nxt.finish
698 local after_last = last_nonblank_in_list and range.start > last_nonblank_in_list
699 local in_indent = nxt and nxt.indent and range.start <= nxt.start and range.start >= (nxt.start - nxt.indent)
700 local placeholder = "asdf"
701 local newpos = margin or range.start
702 if not parent.is_empty then
703 if in_indent then
704 write(newpos, (placeholder.."\n"))
705 else
706 write(newpos, "\n"..((after_last or last_on_line or margin) and placeholder or ""))
709 parser.tree.rewind(parent.start or newpos)
710 -- move the cursor onto the placeholder, so refmt_at can restore the position:
711 local r = {start = newpos, finish = newpos}
712 newpos = walker.start_after(r) or newpos
713 local rangeng = {start = newpos, finish = newpos}
714 newpos = refmt_at(parent, rangeng)
715 local _, parentng = walker.sexp_at({start = newpos, finish = newpos}, true)
716 if after_last then
717 local autoindent = parentng[#parentng].indent
718 write(parentng.finish, "\n"..string.rep(" ", autoindent or 0))
720 if after_last or last_on_line or margin or in_indent then
721 delete(newpos, #placeholder)
723 parser.tree.rewind(parentng.start or newpos)
724 return newpos
727 local function close_and_newline(range, closing)
728 local opening = closing and parser.opposite[closing]
729 local parent = walker.quasilist_at(range, opening)
730 if parent then
731 local has_eol = parent.is_line_comment
732 local r = {start = parent.finish, finish = parent.finish}
733 local newpos = refmt_at(parent, r)
734 r = {start = newpos + (has_eol and 0 or 1), finish = newpos + (has_eol and 0 or 1)}
735 local list, parentng = walker.sexp_at(r)
736 newpos = list and list.finish + 1 or r.start
737 newpos = newline(parentng, {start = newpos, finish = newpos})
738 if not newpos then
739 newpos = r.finish + 1
740 write(newpos, "\n")
741 parser.tree.rewind(parentng.start or list.start)
743 return newpos
744 elseif closing == "\n" then
745 local eol = eol_at(range.start)
746 local _, parentng = walker.sexp_at({start = eol, finish = eol})
747 local newpos = newline(parentng, {start = eol, finish = eol})
748 return newpos
750 return range.start
753 local function join_line(_, pos)
754 local eol = eol_at(pos)
755 local r = {start = eol, finish = eol + 1}
756 return delete_nonsplicing(r, r.start, delete)
759 local wrap_doublequote = make_wrap'"'
761 local function meta_doublequote(range, pos, auto_square)
762 local escaped = walker.escaped_at(range)
763 if not escaped then
764 return wrap_doublequote(range, pos, nil, auto_square)
765 elseif escaped.is_string then
766 return close_and_newline(range, '"')
768 return pos
771 local block_comment_start
772 for o, c in pairs(parser.opposite) do
773 if #c > 1 then
774 block_comment_start = o
775 break
779 local wrap_comment = make_wrap(block_comment_start)
781 return {
782 delete_splicing = delete_splicing,
783 delete_nonsplicing = delete_nonsplicing,
784 refmt_at = refmt_at,
785 pick_out = pick_out,
786 raise_sexp = raise_sexp,
787 slurp_sexp = slurp_sexp,
788 barf_sexp = barf_sexp,
789 splice_sexp = splice_sexp,
790 wrap_round = make_wrap"(",
791 wrap_square = make_wrap"[",
792 wrap_curly = make_wrap"{",
793 meta_doublequote = meta_doublequote,
794 wrap_comment = wrap_comment,
795 insert_pair = insert_pair,
796 newline = newline,
797 join_line = join_line,
798 close_and_newline = close_and_newline,
799 cycle_wrap = cycle_wrap,
800 split_sexp = split_sexp,
801 join_sexps = join_sexps,
802 transpose_sexps = transpose_sexps,
803 transpose_words = transpose_words,
804 transpose_chars = transpose_chars,
808 return M