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