add quasilist_at
[lisp-parkour.git] / edit.lua
blob11709117789969fea763d2b080b697bdcd016cb2
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 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 if last_distinguished and i > 1 then
46 delta = delta + 1
47 if i - 1 <= last_distinguished then
48 delta = delta + 2
49 end
50 elseif i == 1 then
51 -- remove leading space, if any
52 delta = -(s.start - pstart)
53 elseif -- align further arguments below the second one
54 parent.d == "(" -- [] and {} contain data, so no arguments
55 and not parent[1].is_string -- a string can't have arguments
56 --and not parent[1].is_list -- wrong, GNU Emacs compatible behaviour
57 then
58 if i > first_argument + 1 and not parent[first_argument + 1].indent then
59 delta = base_indent + parent[1].finish + 2 - pstart - s.indent - op_adj - (indent_adj or 0)
60 end
61 if delta < 0 then delta = math.max(-s.indent, delta) end
62 end
63 if delta ~= 0 then
64 table.insert(deltas, {s.start, delta})
65 end
66 adj = adj - delta
67 end
68 return adj
69 end
71 local function refmt_list(parent, base_indent, padj, deltas, keep_electric_space)
72 local adj = 0
73 local op_adj = 0
74 local indent_adj = 0
75 local pstart = parent.start + (parent.p and #parent.p or 0) + #parent.d
76 if parent.is_empty then
77 normalize_spacing(pstart, parent.finish - pstart, deltas)
78 return deltas
79 end
80 local last_distinguished = fmt:last_distinguished(parent)
81 local first_argument = 1
82 if parent[1].is_list and #parent > 2 then
83 local op = parent[1]
84 local nearest_indented = op[#op].indent and op[#op] or walker.indented_before(op[#op])
85 indent_adj = nearest_indented.start - pstart - nearest_indented.indent + 1
86 end
87 for i, s in ipairs(parent) do
88 adj = s.indent and 0 or adj
89 if last_distinguished then
90 -- do not let early comments influence the indentation of real expressions:
91 if i <= last_distinguished and s.is_comment then
92 last_distinguished = last_distinguished + 1
93 end
94 -- s is the name of a named let. account for it:
95 if i == last_distinguished + 1 and not s.is_list and parent[1].text == "let" then
96 last_distinguished = last_distinguished + 1
97 end
98 -- do not let early comments influence the indentation of real expressions:
99 elseif i <= first_argument + 1 and s.is_comment then
100 first_argument = first_argument + 1
102 local has_eol = s.is_comment and parser.opposite[s.d] == "\n"
103 adj = leading_space(s, deltas, adj, parent, i, pstart, has_eol)
104 adj = trailing_space(s, deltas, adj, parent, i, has_eol, keep_electric_space)
105 adj = indentation(s, deltas, adj, parent, i, pstart, base_indent, last_distinguished, first_argument,
106 op_adj, indent_adj)
107 if i == 1 then
108 op_adj = adj
110 if s.is_list then
111 local nearest_indented = s.indent and s or walker.indented_before(s)
112 local parent_column = nearest_indented and
113 nearest_indented.indent + (s.start + (s.p and #s.p or 0) + #s.d) - nearest_indented.start
114 or base_indent + (s.start + (s.p and #s.p or 0) + #s.d) - pstart
115 refmt_list(s, parent_column - adj, padj + adj, deltas, keep_electric_space)
118 return deltas
121 local function refmt_at(scope, range, keep_electric_space)
122 if not range or not scope or scope.is_root then return end
123 local parent = walker.sexp_at(scope, true)
124 if not (parent and parent.is_list) then return end
125 local _, parent_at, m = walker.sexp_at(range, true)
126 if not m then
127 _, m = parent_at.after(range.start, startof)
128 if not m then
129 m = #parent_at
132 local base = parent_at[m] or parent_at
133 local path = walker.sexp_path(base)
134 local after_start, at_pfinish
135 if parent_at[m] then
136 local s = parent_at[#parent_at]
137 local has_eol = s.is_comment and parser.opposite[s.d] == "\n"
138 if range.start >= s.finish + 1 + (has_eol and 1 or 0) then
139 at_pfinish = true
140 else
141 after_start = range.start < base.start and 0 or range.start - base.start
143 else
144 after_start = (parent_at.p and #parent_at.p or 0) + #parent_at.d
146 local indented_parent = parent.indent and parent or parent.is_list and walker.indented_before(parent)
147 local parent_column = indented_parent and (indented_parent.indent + parent.start - indented_parent.start +
148 (parent.p and #parent.p or 0) + #parent.d) or
150 local deltas = refmt_list(parent, parent_column, 0, {}, keep_electric_space)
151 table.sort(deltas, function(d1, d2) return d1[1] > d2[1] end)
152 for _, pair in ipairs(deltas) do
153 local offset, delta = unpack(pair)
154 if delta > 0 then
155 write(offset, string.rep(" ", delta))
156 elseif delta < 0 then
157 delete(offset + delta, -delta)
160 parser.tree.rewind(parent.start or 0)
161 if path then
162 local sexp, parentng = walker.goto_path(path)
163 if sexp then
164 if at_pfinish then
165 return parentng.finish
166 else
167 return sexp.start + after_start
171 return range.start
174 local function splice(pos, sexps, skip, backwards, action)
175 local spliced
176 local sexp = walker.sexp_at(skip, true)
177 local start = sexp.start + (sexp.p and #sexp.p or 0)
178 local is_line_comment = sexp.is_comment and parser.opposite[sexp.d] == "\n"
179 -- XXX: don't splice empty line comments _yet_; see _join_or_splice
180 local tosplice = action.splice or action.wrap or not is_line_comment and sexp.is_empty
181 local opening = (sexp.p or '')..sexp.d
182 local closing = parser.opposite[sexp.d]
183 local real_start = tosplice and sexp.start or start + #sexp.d
184 local splice_closing = closing ~= "\n" or sexp.is_empty
185 local real_finish = sexp.finish + 1 - (tosplice and splice_closing and 0 or #closing)
186 local first = backwards and
187 {start = real_start, finish = math.max(pos, start + #sexp.d)} or
188 {start = real_start, finish = sexp.is_empty and pos or start + #sexp.d}
189 local second = backwards and
190 {start = sexp.is_empty and pos or sexp.finish + 1 - #closing, finish = real_finish} or
191 {start = math.min(pos, sexp.finish + 1 - #closing), finish = real_finish}
192 action.func(second)
193 action.func(first)
194 spliced = tosplice
195 if action.kill then
196 local ndeleted = first.finish - first.start + second.finish - second.start
197 if ndeleted > 0 then
198 sexps.rewind(sexp.start)
201 return first.finish - first.start, spliced, opening, closing
204 -- If you try to delete some part of indentation, this function joins the current line with
205 -- the previous one, unless the latter is a line comment.
206 -- it does this by
207 -- a) extending or shrinking the range that is to be deleted
208 -- b) returning a truthy value to trick pick_out to glide the cursor, but refmt to restore the deleted spaces
209 -- c) both a) and b)
210 local function delete_indentation(range, pos)
211 local node, parent = walker.sexp_at(range)
212 if node or not parent.is_list then return end
213 local prev = parent.before(pos, finishof)
214 local nxt = parent.after(pos, startof)
215 if prev and prev.finish >= range.start or nxt and nxt.start < range.finish then return end
216 local backwards = pos == range.finish
217 local adj = 0
218 local eol = eol_at(pos)
219 local on_empty_line = not nxt or eol and nxt.start > eol
220 local empty_line_after_comment = prev and prev.is_comment and parser.opposite[prev.d] == "\n"
221 and on_empty_line
222 if nxt and nxt.indent then
223 if backwards and prev then
224 range.start = prev.finish + (empty_line_after_comment and 0 or 1)
225 if not prev.d then
226 range.finish = range.start + 1
227 adj = (on_empty_line and 0 or 1)
230 return adj
232 if not nxt and prev then
233 range.start = prev.finish + 1
234 range.finish = parent.finish
235 elseif nxt and nxt.start == range.finish and (not prev or prev.finish + 1 == range.start) then
236 if prev and prev.d or nxt and nxt.d then
237 -- don't delete spaces near delimiters, just slide the cursor:
238 if backwards then
239 range.finish = range.start
240 else
241 range.start = range.finish
244 return adj
248 local function big_enough_parent(pos1, pos2)
249 -- since the pos1-pos2 range can cross list boundaries, find which list contains both pos1 and pos2
250 local _, p1, p2
251 _, p1 = walker.sexp_at({start = pos1, finish = pos1}, true)
252 if p1.is_root or not (p1.start < pos1 and p1.finish > pos2) then
253 _, p2 = walker.sexp_at({start = pos2, finish = pos2}, true)
255 return p2 and not p2.is_root and p2 or p1
258 local function pick_out(range, pos, action)
259 local ndeleted = 0
260 if range.start == range.finish then return ndeleted end
261 local sexps = parser.tree
262 local skips = sexps.unbalanced_delimiters(range)
263 -- handle splice and kill-splice of forms and strings:
264 if #skips == 1 then
265 local sexp = walker.sexp_at(skips[1], true)
266 local backward_splice = skips[1].opening and pos >= sexp.start + (sexp.p and #sexp.p or 0) + #sexp.d
267 and range.start >= sexp.start
268 local forward_splice = skips[1].closing and pos <= sexp.finish + 1 - #parser.opposite[sexp.d]
269 and range.finish <= sexp.finish + 1
270 if backward_splice or forward_splice then
271 return splice(backward_splice and range.finish or range.start, sexps, sexp, backward_splice, action)
274 local node, parent = walker.sexp_at({start = range.finish, finish = range.finish})
275 local drop_eol = node and node.finish + 1 == range.finish and node.is_comment and parser.opposite[node.d] == "\n"
276 local par = big_enough_parent(range.start, range.finish)
277 local operator_changed = par[1] and par[1].finish >= range.start
278 local refmt = #skips == 0 and delete_indentation(range, pos) or operator_changed and 0
279 table.sort(skips, function(a, b) return a.start < b.start end)
280 table.insert(skips, {start = range.finish - (drop_eol and 1 or 0)})
281 table.insert(skips, 1, {finish = range.start})
282 ndeleted = ndeleted + (drop_eol and 1 or 0)
283 for i = #skips - 1, 1, -1 do
284 local region = {start = skips[i].finish, finish = skips[i + 1].start}
285 if skips[i].closing and skips[i + 1].opening then
286 -- leave out some of the space between adjacent lists
287 local _, rparent = walker.sexp_at(region)
288 local nxt = rparent.after(region.start, startof)
289 region.start = nxt and nxt.start or region.start
291 if action then
292 action.func(region)
293 ndeleted = ndeleted + (region.finish - region.start)
296 -- if parent[#parent + 1] is nil, we are at EOF
297 if ndeleted > 0 and (not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1]) then
298 sexps.rewind(range.start)
300 return ndeleted - (refmt or 0), nil, nil, nil, refmt
303 local function raise_sexp(range, pos)
304 local sexp, parent = walker.sexp_at(range, true)
305 if sexp and parent and parent.is_list then
306 delete(sexp.finish + 1, parent.finish - sexp.finish)
307 delete(parent.start, sexp.start - parent.start)
308 parser.tree.rewind(parent.start)
309 range.start = parent.start + pos - sexp.start
310 range.finish = range.start
311 local _, nodes = walker.sexp_path(range)
312 local grandparent = nodes[#nodes - 2]
313 return grandparent and refmt_at(grandparent, range) or range.start
317 local function slurp_sexp(range, forward)
318 local _, parent = walker.sexp_at(range, true)
319 local seeker = forward and walker.finish_after or walker.start_before
320 if not parent or not parent.is_list then return range.start end
321 local r = {start = parent.start, finish = parent.finish + 1}
322 local newpos = seeker(r, is_comment)
323 if not newpos then return range.start end
324 local opening = (parent.p or '')..parent.d
325 local closing = parser.opposite[parent.d]
326 local delimiter = forward and closing or opening
327 if forward then
328 write(newpos, delimiter)
330 delete(forward and parent.finish or parent.start, #delimiter)
331 if not forward then
332 write(newpos, delimiter)
334 parser.tree.rewind(math.min(parent.start, newpos))
335 return refmt_at(big_enough_parent(newpos, range.start), range) or range.start
338 local function barf_sexp(range, forward)
339 local _, parent, m = walker.sexp_at(range, true)
340 local seeker = forward and walker.finish_before or walker.start_after
341 -- TODO: barfing out of strings requires calling the parser on them
342 if not parent or not parent.is_list then return range.start end
343 local opening = (parent.p or '')..parent.d
344 local pstart = parent.start + #opening
345 local r = {start = forward and parent.finish - 1 or pstart, finish = forward and parent.finish or pstart + 1}
346 local newpos = seeker(r, is_comment) or forward and pstart or parent.finish
347 if not newpos then return range.start end
348 local closing = parser.opposite[parent.d]
349 local delimiter = forward and closing or opening
350 if not forward then
351 write(newpos, delimiter)
353 delete(forward and parent.finish or parent.start, #delimiter)
354 if forward then
355 write(newpos, delimiter)
357 parser.tree.rewind(math.min(parent.start, newpos))
358 local barfed_cursor_backward = m == 1 and not forward
359 local barfed_cursor_forward = m == #parent and forward
360 range.start = range.start + #delimiter * (barfed_cursor_backward and -1 or barfed_cursor_forward and 1 or 0)
361 return refmt_at(big_enough_parent(newpos, parent.finish + 1), range) or range.start
364 local function splice_sexp(range, _, no_refmt)
365 local _, parent = walker.sexp_at(range)
366 if not parent or not parent.d then return end
367 local opening = (parent.p or '')..parent.d
368 local closing = parser.opposite[parent.d]
369 local finish = parent.finish + 1 - #closing
370 if closing ~= "\n" then
371 delete(finish, #closing)
373 -- TODO: (un)escape special characters, if necessary
374 delete(parent.start, parent.is_empty and (finish - parent.start) or #opening)
375 parser.tree.rewind(parent.start)
376 range.start = range.start - #opening
377 range.finish = range.start
378 local _, parentng = walker.sexp_at(range, true)
379 return not no_refmt and refmt_at(parentng, range) or range.start
382 local function rewrap(parent, kind)
383 local pstart = parent.start + #((parent.p or '')..parent.d) - 1
384 delete(parent.finish, 1)
385 write(parent.finish, parser.opposite[kind])
386 delete(pstart, #parent.d)
387 write(pstart, kind)
388 parser.tree.rewind(parent.start)
391 local function cycle_wrap(range, pos)
392 local _, parent = walker.sexp_at(range)
393 if not parent or not parent.is_list then return end
394 local next_kind = {["("] = "[", ["["] = "{", ["{"] = "("}
395 rewrap(parent, next_kind[parent.d])
396 return pos
399 local function split_sexp(range)
400 local _, parent = walker.sexp_at(range)
401 if not (parent and parent.d) then return end
402 local new_finish, new_start
403 if parent.is_list then
404 local prev = parent.before(range.start, finishof, is_comment)
405 new_finish = prev and prev.finish + 1
406 -- XXX: do not skip comments here, so they end up in the second list
407 -- and are not separated from their target expression:
408 local nxt = new_finish and parent.after(new_finish, startof)
409 new_start = nxt and nxt.start
410 else
411 new_start = range.start
412 new_finish = range.start
414 if not (new_start and new_finish) then return end
415 local opening = (parent.p or '')..parent.d
416 local closing = parser.opposite[parent.d]
417 write(new_start, opening)
418 local sep = parser.opposite[parent.d] == "\n" and "" -- line comments already have a separator
419 or new_finish == new_start and " " -- only add a separator if there was none before
420 or ""
421 write(new_finish, closing..sep)
422 parser.tree.rewind(parent.start)
423 range.start = new_start + (parent.is_list and 0 or #opening + #closing)
424 range.finish = range.start
425 local _, nodes = walker.sexp_path(range)
426 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
427 local scope = parentng and not parentng.is_root and parentng or grandparent
428 return refmt_at(scope, range) or range.start
431 local function join_sexps(range)
432 local node, parent = walker.sexp_at(range, true)
433 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
434 local second = first ~= node and node or parent.after(range.start, startof)
435 if not (first and second and first.d and
436 -- don't join line comments to margin comments:
437 (parser.opposite[first.d] ~= "\n" or first.indent and second.indent) and
438 (first.d == second.d or
439 -- join line comments even when their delimiters differ slightly
440 -- (different number of semicolons, existence/lack of a space after them)
441 parser.opposite[first.d] == parser.opposite[second.d])) then
442 return
444 local opening = (second.p or '')..second.d
445 local closing = parser.opposite[first.d]
446 local pos
447 if not first.is_list then
448 pos = first.finish + 1 - #closing
449 delete(pos, second.start + #opening - pos)
450 else
451 delete(second.start, #opening)
452 delete(first.finish, #closing)
453 pos = second.start - #closing
455 parser.tree.rewind(first.start)
456 range.start = pos
457 range.finish = range.start
458 local _, nodes = walker.sexp_path(range)
459 local parentng, grandparent = nodes[#nodes - 1], nodes[#nodes - 2]
460 local scope = parentng and not parentng.is_root and parentng or grandparent
461 return refmt_at(scope, range) or range.start
464 local function delete_splicing(range, pos, splicing, delete_and_yank)
465 local action = {kill = true, wrap = splicing, splice = splicing, func = delete_and_yank}
466 local sexp, parent, n = walker.sexp_at(range, true)
467 local ndeleted, spliced = pick_out(range, pos, action)
468 local closing = spliced and parser.opposite[sexp.d]
469 local inner_list_len = spliced and sexp.finish - sexp.start + 1 - #sexp.d - #closing
470 local range_len = range.finish - range.start
471 local backwards = pos == range.finish
472 local whole_object = sexp and
473 (sexp.start == range.start and sexp.finish + 1 == range.finish
474 or spliced and (inner_list_len <= (backwards and range_len + ndeleted or range_len)))
475 local in_head_atom = sexp and not sexp.d and n == 1 and #parent > 1
476 local in_whitespace = not sexp
477 if whole_object or in_whitespace or in_head_atom then
478 local cur = whole_object and sexp.start or range.start
479 -- if parent[#parent + 1] is nil, we are at EOF
480 if not parent.is_root or parent.is_parsed(cur) or parent[#parent + 1] then
481 parser.tree.rewind(cur)
483 local r = {start = cur, finish = cur}
484 local _, parentng = walker.sexp_at(r, true)
485 local newpos = refmt_at(parentng, r)
486 return newpos or cur
488 return spliced and (backwards and sexp.start or range.start - ndeleted)
489 or not backwards and ndeleted <= 0 and range.finish - ndeleted
490 or range.start
493 local function transpose(range, first, second)
494 if not (first and second) then return end
495 local copy1 = first.text
496 local copy2 = second.text
497 delete(second.start, second.finish + 1 - second.start)
498 write(second.start, copy1)
499 delete(first.start, first.finish + 1 - first.start)
500 write(first.start, copy2)
501 parser.tree.rewind(first.start)
502 range.start = second.finish + 1
503 range.finish = range.start
504 return refmt_at(big_enough_parent(first.start, second.finish), range) or range.start
507 local function transpose_sexps(range)
508 local node, parent = walker.sexp_at(range, true)
509 local first = node and node.finish + 1 == range.start and node or parent.before(range.start, finishof)
510 local second = first ~= node and node or parent.after(range.start, startof)
511 return transpose(range, first, second)
514 local function transpose_words(range)
515 local _, first = walker.start_float_before(range)
516 local _, second = walker.finish_float_after(range)
517 if first and second and first.start == second.start then
518 _, first = walker.start_float_before(first)
520 return transpose(range, first, second)
523 local function transpose_chars(range)
524 local node, parent, i = walker.sexp_at(range)
525 local nxt = i and parent[i + 1]
526 local pstart = not parent.is_root and parent.start + (parent.p and #parent.p or 0) + #parent.d
527 local pfinish = not parent.is_root and parent.finish
528 local npref = node and node.p and node.start + #node.p
529 -- only allow transposing while inside atoms/words and prefixes
530 if node and ((npref and (range.start <= npref and range.start > node.start) or
531 node.d and range.start > node.finish + 1) or not node.d and (not pstart or range.start > pstart)) then
532 local start = range.start -
533 ((range.start == pfinish or range.start == npref or
534 range.start == node.finish + 1 and (parent.is_list or parent.is_root) and (not nxt or nxt.indent)) and 1 or 0)
535 local str_start = start - node.start + 1
536 local char = node.text:sub(str_start, str_start)
537 delete(start, 1)
538 write(start - 1, #char > 0 and char or " ")
539 parser.tree.rewind(start)
540 local in_head_atom = i == 1 and #parent > 1
541 return parent.is_list and in_head_atom
542 and refmt_at(parent, {start = start + 1, finish = start + 1})
543 or start + 1
547 local function _join_or_splice(parent, n, range, pos)
548 local sexp = parent[n]
549 local nxt = parent[n + 1]
550 local is_last = not nxt or parser.opposite[nxt.d] ~= "\n"
551 local newpos = not (is_last and sexp.is_empty) and join_sexps(range)
552 if not newpos and sexp.is_empty then
553 newpos = splice_sexp({start = pos, finish = pos}, nil, true)
555 return newpos or pos
558 local function delete_nonsplicing(range, pos, delete_maybe_yank)
559 local action = {kill = true, wrap = false, splice = false, func = delete_maybe_yank}
560 local ndeleted, spliced, opening, _, refmt = pick_out(range, pos, action)
561 local backwards = pos == range.finish
562 if opening then
563 if spliced then
564 return pos - ndeleted
565 else
566 if ndeleted == 0 then
567 local closing = parser.opposite[opening] -- XXX: why don't I use the pick_out return value?
568 if closing == "\n" then
569 local sexp, parent, n = walker.sexp_at(range)
570 if pos == sexp.start + #sexp.d and backwards then
571 return _join_or_splice(parent, n, range, pos)
572 elseif pos == sexp.finish then
573 local r = {start = pos + #closing, finish = pos + #closing}
574 return _join_or_splice(parent, n, r, pos)
578 return backwards and (range.finish - ndeleted) or range.start
580 else
581 local newpos = backwards and range.start or (range.finish - ndeleted)
582 if refmt then
583 local r = {start = newpos, finish = newpos}
584 return refmt_at(big_enough_parent(range.start, range.finish - ndeleted), r, true)
586 return newpos
590 local function insert_pair(range, delimiter, shiftless, auto_square)
591 local indices, nodes = walker.sexp_path(range)
592 local sexp = nodes[#nodes]
593 local right_after_prefix = sexp and sexp.p and range.start == sexp.start + #sexp.p
594 -- XXX: here I assume that # is a valid prefix for the dialect
595 local mb_closing = (delimiter == "|") and right_after_prefix and parser.opposite[sexp.p .. delimiter]
596 local closing = mb_closing or parser.opposite[delimiter]
597 local squarewords = fmt.squarewords
598 if squarewords and not right_after_prefix
599 and not (closing == "]" and shiftless and not auto_square)
600 and (auto_square or squarewords.not_optional)
601 and not fmt:adjust_bracket_p(indices, nodes, range) then
602 delimiter, closing = "[", "]"
604 write(range.finish, closing)
605 write(range.start, delimiter)
606 if right_after_prefix or parser.tree.is_parsed(range.start) then
607 parser.tree.rewind(right_after_prefix and sexp.start or range.start)
609 return range.start + #delimiter
612 local function make_wrap(kind)
613 return function(range, pos, auto_square)
614 local opening = kind
615 local function _wrap(r)
616 if not (r.finish > r.start) then return end
617 insert_pair(r, opening, false, auto_square)
619 local action = {kill = false, wrap = false, splice = false, func = _wrap}
620 pick_out(range, pos, action)
621 range.start = range.start + #opening
622 return refmt_at(big_enough_parent(range.start - #opening, range.start), range) or range.start
626 local function newline(parent, range)
627 local line_comment = parent.d and parser.opposite[parent.d] == "\n"
628 -- do not autoextend margin comments:
629 if line_comment and range.start < parent.finish + (parent.indent and 1 or 0) then
630 local newpos = split_sexp(range)
631 if newpos then
632 return newpos
635 if not parent.is_list and not line_comment then
636 -- if parent[#parent + 1] is nil, we are at EOF
637 if not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
638 parser.tree.rewind(parent.start or range.start)
640 return
642 if not parent.is_list then
643 local _
644 _, parent = walker.sexp_at(parent, true)
646 local last_nonblank_in_list = not parent.is_empty and parent[#parent].finish - (line_comment and 1 or 0)
647 local nxt = parent.after(range.start, startof)
648 local last_on_line = not nxt or nxt and nxt.indent and nxt.start > range.start
649 local margin = nxt and not nxt.indent and nxt.is_comment and parser.opposite[nxt.d] == "\n" and nxt.finish
650 local after_last = last_nonblank_in_list and range.start > last_nonblank_in_list
651 local in_indent = nxt and nxt.indent and range.start <= nxt.start and range.start >= (nxt.start - nxt.indent)
652 local placeholder = "asdf"
653 local newpos = margin or range.start
654 if not parent.is_empty then
655 if in_indent then
656 write(newpos, (placeholder.."\n"))
657 else
658 write(newpos, "\n"..((after_last or last_on_line or margin) and placeholder or ""))
661 parser.tree.rewind(parent.start or newpos)
662 -- move the cursor onto the placeholder, so refmt_at can restore the position:
663 local r = {start = newpos, finish = newpos}
664 newpos = walker.start_after(r) or newpos
665 local rangeng = {start = newpos, finish = newpos}
666 newpos = refmt_at(parent, rangeng) or rangeng.start
667 local _, parentng = walker.sexp_at({start = newpos, finish = newpos}, true)
668 if after_last then
669 local autoindent = parentng[#parentng].indent
670 write(parentng.finish, "\n"..string.rep(" ", autoindent or 0))
672 if after_last or last_on_line or margin or in_indent then
673 delete(newpos, #placeholder)
675 parser.tree.rewind(parentng.start or newpos)
676 return newpos
679 local function close_and_newline(range)
680 local _, parent = walker.sexp_at(range)
681 if parent and not parent.is_root then
682 local r = {start = parent.finish, finish = parent.finish}
683 local newpos = refmt_at(parent, r)
684 r = {start = (newpos or r.finish) + 1, finish = (newpos or r.finish) + 1}
685 local list, parentng = walker.sexp_at(r)
686 newpos = newline(parentng, {start = list.finish + 1, finish = list.finish + 1})
687 if not newpos then
688 newpos = list.finish + 1
689 write(newpos, "\n")
691 return newpos
693 return range.start
696 local function join_line(_, pos)
697 local eol = eol_at(pos)
698 local r = {start = eol, finish = eol + 1}
699 return delete_nonsplicing(r, r.start, delete)
702 local function meta_doublequote(range, pos, auto_square)
703 local escaped = walker.escaped_at(range)
704 if not escaped then
705 local _, parent = walker.sexp_at(range)
706 local newpos = insert_pair(range, '"', false, auto_square)
707 return refmt_at(parent, {start = newpos, finish = newpos}) or newpos
708 elseif escaped.is_string then
709 return close_and_newline(range)
711 return pos
714 return {
715 delete_splicing = delete_splicing,
716 delete_nonsplicing = delete_nonsplicing,
717 refmt_at = refmt_at,
718 pick_out = pick_out,
719 raise_sexp = raise_sexp,
720 slurp_sexp = slurp_sexp,
721 barf_sexp = barf_sexp,
722 splice_sexp = splice_sexp,
723 wrap_round = make_wrap"(",
724 wrap_square = make_wrap"[",
725 wrap_curly = make_wrap"{",
726 meta_doublequote = meta_doublequote,
727 insert_pair = insert_pair,
728 newline = newline,
729 join_line = join_line,
730 close_and_newline = close_and_newline,
731 cycle_wrap = cycle_wrap,
732 split_sexp = split_sexp,
733 join_sexps = join_sexps,
734 transpose_sexps = transpose_sexps,
735 transpose_words = transpose_words,
736 transpose_chars = transpose_chars,
740 return M