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
, insert
, delete
)
12 local function normalize_spacing(start
, delta
, list
)
14 table.insert(list
, {start
+ delta
, -delta
})
16 table.insert(list
, {start
, -delta
})
21 local function reindent_list(parent
, base_indent
, deltas
, keep_electric_space
)
23 local pstart
= parent
.start
+ (parent
.p
and #parent
.p
or 0) + #parent
.d
24 if parent
.is_empty
then
25 normalize_spacing(pstart
, parent
.finish
- pstart
, deltas
)
28 for i
, s
in ipairs(parent
) do
30 adj
= s
.indent
and 0 or adj
31 local prev
= parent
[i
- 1]
32 if not s
.indent
and not (prev
and (s
.d
== "|" or prev
.d
== "|")) then
33 -- remove/add leading spaces
34 local off
= prev
and prev
.finish
+ 1 or pstart
35 adj
= adj
+ normalize_spacing(off
, s
.start
- off
- (prev
and 1 or 0), deltas
)
37 local nxt
= parent
[i
+ 1]
38 local is_last
= not nxt
39 if (is_last
and not keep_electric_space
) or nxt
and nxt
.indent
then
40 -- clean up trailing spaces
41 local has_eol
= s
.is_comment
and parser
.opposite
[s
.d
]:find("\n")
42 local off
= is_last
and parent
.finish
- (has_eol
and i
< #parent
and 1 or 0) or nxt
.start
- nxt
.indent
- 1
43 local finish
= s
.finish
+ (has_eol
and i
< #parent
and 0 or 1)
44 normalize_spacing(finish
, off
- finish
, deltas
)
47 delta
= base_indent
- s
.indent
48 local last_distinguished
= fmt
:last_distinguished(parent
)
49 if last_distinguished
then
51 if i
- 1 <= last_distinguished
then
54 elseif -- align further arguments below the second one
55 parent
.d
== "(" -- [] and {} contain data, so no arguments
56 and not parent
[1].is_string
-- a string can't have arguments
57 --and not parent[1].is_list -- wrong, GNU Emacs compatible behaviour
59 if i
> 2 and not parent
[2].indent
then
60 delta
= base_indent
+ parent
[2].start
- pstart
- s
.indent
63 if delta
< 0 then delta
= math
.max(-s
.indent
, delta
) end
65 table.insert(deltas
, {s
.start
, delta
})
70 local nearest_indented
= s
.indent
and s
or walker
.indented_before(s
)
71 local parent_column
= nearest_indented
.indent
+ (s
.start
+ (s
.p
and #s
.p
or 0) + #s
.d
) - nearest_indented
.start
72 reindent_list(s
, parent_column
- adj
, deltas
, keep_electric_space
)
78 -- FIXME: when called at the end of another operation, some nested lists do not get reindented
79 local function reindent_at(scope
, range
, keep_electric_space
)
80 if not range
or scope
.is_root
then return end
81 local parent
= walker
.sexp_at(scope
, true)
82 if not (parent
and parent
.is_list
) then return end
83 local _
, parent_at
, m
= walker
.sexp_at(range
, true)
85 _
, m
= parent_at
.after(range
.start
, startof
)
90 local base
= parent_at
[m
] or parent_at
91 local path
= walker
.sexp_path(base
)
92 local after_start
, at_pfinish
94 local s
= parent_at
[#parent_at
]
95 local has_eol
= s
.is_comment
and parser
.opposite
[s
.d
]:find("\n")
96 if range
.start
>= s
.finish
+ 1 + (has_eol
and 1 or 0) then
99 after_start
= range
.start
< base
.start
and 0 or range
.start
- base
.start
102 after_start
= (parent_at
.p
and #parent_at
.p
or 0) + #parent_at
.d
104 local indented_parent
= parent
.indent
and parent
or parent
.is_list
and walker
.indented_before(parent
)
105 local parent_column
= indented_parent
and (indented_parent
.indent
+ parent
.start
- indented_parent
.start
+
106 (parent
.p
and #parent
.p
or 0) + #parent
.d
) or
108 local deltas
= reindent_list(parent
, parent_column
, {}, keep_electric_space
)
109 table.sort(deltas
, function(d1
, d2
) return d1
[1] > d2
[1] end)
110 for _
, pair
in ipairs(deltas
) do
111 local offset
, delta
= unpack(pair
)
113 insert(offset
, string.rep(" ", delta
))
114 elseif delta
< 0 then
115 delete(offset
+ delta
, -delta
)
118 parser
.tree
.rewind(parent
.start
or 0)
120 local sexp
, parentng
= walker
.goto_path(path
)
123 return parentng
.finish
125 return sexp
.start
+ after_start
132 local function splice(pos
, sexps
, skip
, backwards
, action
)
134 local sexp
= walker
.sexp_at(skip
, true)
135 local start
= sexp
.start
+ (sexp
.p
and #sexp
.p
or 0)
136 local tosplice
= action
.splice
or action
.wrap
or sexp
.is_empty
137 local opening
= (sexp
.p
or '')..sexp
.d
138 local closing
= parser
.opposite
[sexp
.d
]
139 local real_start
= tosplice
and sexp
.start
or start
+ #sexp
.d
140 local splice_closing
= not closing
:find("\n") or sexp
.is_empty
141 local real_finish
= sexp
.finish
+ 1 - (tosplice
and splice_closing
and 0 or #closing
)
142 local first
= backwards
and
143 {start
= real_start
, finish
= math
.max(pos
, start
+ #sexp
.d
)} or
144 {start
= real_start
, finish
= sexp
.is_empty
and pos
or start
+ #sexp
.d
}
145 local second
= backwards
and
146 {start
= sexp
.is_empty
and pos
or sexp
.finish
+ 1 - #closing
, finish
= real_finish
} or
147 {start
= math
.min(pos
, sexp
.finish
+ 1 - #closing
), finish
= real_finish
}
152 local ndeleted
= first
.finish
- first
.start
+ second
.finish
- second
.start
154 sexps
.rewind(sexp
.start
)
157 return first
.finish
- first
.start
, spliced
, opening
, closing
160 local function pick_out(range
, pos
, action
)
161 local sexps
= parser
.tree
162 local skips
= sexps
.unbalanced_delimiters(range
)
163 -- handle splice and kill-splice of forms and strings:
165 local sexp
= walker
.sexp_at(skips
[1], true)
166 local backward_splice
= skips
[1].opening
and pos
>= sexp
.start
+ (sexp
.p
and #sexp
.p
or 0) + #sexp
.d
167 and range
.start
>= sexp
.start
168 local forward_splice
= skips
[1].closing
and pos
<= sexp
.finish
+ 1 - #parser
.opposite
[sexp
.d
]
169 and range
.finish
<= sexp
.finish
+ 1
170 if backward_splice
or forward_splice
then
171 return splice(backward_splice
and range
.finish
or range
.start
, sexps
, sexp
, backward_splice
, action
)
174 local node
, parent
= walker
.sexp_at({start
= range
.finish
, finish
= range
.finish
})
175 local drop_eol
= node
and node
.finish
+ 1 == range
.finish
and node
.is_comment
and parser
.opposite
[node
.d
]:find("\n")
176 table.sort(skips
, function(a
, b
) return a
.start
< b
.start
end)
177 table.insert(skips
, {start
= range
.finish
- (drop_eol
and 1 or 0)})
178 table.insert(skips
, 1, {finish
= range
.start
})
179 local ndeleted
= drop_eol
and 1 or 0
180 for i
= #skips
- 1, 1, -1 do
181 local region
= {start
= skips
[i
].finish
, finish
= skips
[i
+ 1].start
}
182 if skips
[i
].closing
and skips
[i
+ 1].opening
then
183 -- leave out some of the space between adjacent lists
184 local _
, rparent
= walker
.sexp_at(region
)
185 local nxt
= rparent
.after(region
.start
, startof
)
186 region
.start
= nxt
and nxt
.start
or region
.start
190 ndeleted
= ndeleted
+ (region
.finish
- region
.start
)
193 -- if parent[#parent + 1] is nil, we are at EOF
194 if ndeleted
> 0 and (not parent
.is_root
or parent
[#parent
+ 1]) then
195 sexps
.rewind(range
.start
)
200 local function raise_sexp(range
, pos
)
201 local sexp
, parent
= walker
.sexp_at(range
, true)
202 if sexp
and parent
and parent
.is_list
then
203 delete(sexp
.finish
+ 1, parent
.finish
- sexp
.finish
)
204 delete(parent
.start
, sexp
.start
- parent
.start
)
205 parser
.tree
.rewind(parent
.start
)
206 range
.start
= parent
.start
+ pos
- sexp
.start
207 range
.finish
= range
.start
208 local _
, parentng
= walker
.sexp_at(range
, true)
209 local _
, grandparent
= walker
.sexp_at(parentng
, true)
210 return grandparent
and reindent_at(grandparent
, range
) or range
.start
214 local function slurp_sexp(range
, forward
)
215 local _
, parent
= walker
.sexp_at(range
, true)
216 local seeker
= forward
and walker
.finish_after
or walker
.start_before
217 if not parent
or not parent
.is_list
then return range
.start
end
218 local r
= {start
= parent
.start
, finish
= parent
.finish
+ 1}
219 local newpos
= seeker(r
, is_comment
)
220 if not newpos
then return range
.start
end
221 local opening
= (parent
.p
or '')..parent
.d
222 local closing
= parser
.opposite
[parent
.d
]
223 local delimiter
= forward
and closing
or opening
225 insert(newpos
, delimiter
)
227 delete(forward
and parent
.finish
or parent
.start
, #delimiter
)
229 insert(newpos
, delimiter
)
231 parser
.tree
.rewind(math
.min(parent
.start
, newpos
))
232 local _
, parentng
= walker
.sexp_at(range
, true)
233 return reindent_at(parentng
, range
)
236 local function barf_sexp(range
, forward
)
237 local _
, parent
, m
= walker
.sexp_at(range
, true)
238 local seeker
= forward
and walker
.finish_before
or walker
.start_after
239 -- TODO: barfing out of strings requires calling the parser on them
240 if not parent
or not parent
.is_list
then return range
.start
end
241 local opening
= (parent
.p
or '')..parent
.d
242 local pstart
= parent
.start
+ #opening
243 local r
= {start
= forward
and parent
.finish
- 1 or pstart
, finish
= forward
and parent
.finish
or pstart
+ 1}
244 local newpos
= seeker(r
, is_comment
) or forward
and pstart
or parent
.finish
245 if not newpos
then return range
.start
end
246 local closing
= parser
.opposite
[parent
.d
]
247 local delimiter
= forward
and closing
or opening
249 insert(newpos
, delimiter
)
251 delete(forward
and parent
.finish
or parent
.start
, #delimiter
)
253 insert(newpos
, delimiter
)
255 parser
.tree
.rewind(math
.min(parent
.start
, newpos
))
256 range
.start
= range
.start
+ #delimiter
*
257 (m
== 1 and not forward
and -1 or m
== #parent
and forward
and 1 or 0)
258 local _
, parentng
= walker
.sexp_at(range
, true)
259 local _
, grandparent
= walker
.sexp_at(parentng
, true)
260 return reindent_at(grandparent
, range
) or range
.start
263 local function splice_sexp(range
, _
)
264 local _
, parent
= walker
.sexp_at(range
)
265 if not parent
or not parent
.d
then return end
266 local opening
= (parent
.p
or '')..parent
.d
267 local closing
= parser
.opposite
[parent
.d
]
268 local finish
= parent
.finish
+ 1 - #closing
269 if not closing
:find("\n") then
270 delete(finish
, #closing
)
272 -- TODO: (un)escape special characters, if necessary
273 delete(parent
.start
, parent
.is_empty
and (finish
- parent
.start
) or #opening
)
274 parser
.tree
.rewind(parent
.start
)
275 range
.start
= range
.start
- #opening
276 range
.finish
= range
.start
277 local _
, parentng
= walker
.sexp_at(range
, true)
278 return reindent_at(parentng
, range
)
281 local function cycle_wrap(range
, pos
)
282 local _
, parent
= walker
.sexp_at(range
)
283 if not parent
or not parent
.is_list
then return end
284 local pstart
= parent
.start
+ #((parent
.p
or '')..parent
.d
) - 1
285 local next_kind
= {["("] = "[", ["["] = "{", ["{"] = "("}
286 delete(parent
.finish
, 1)
287 insert(parent
.finish
, parser
.opposite
[next_kind
[parent
.d]]
)
288 delete(pstart
, #parent
.d
)
289 insert(pstart
, next_kind
[parent
.d
])
290 parser
.tree
.rewind(parent
.start
)
294 local function split_sexp(range
)
295 local _
, parent
= walker
.sexp_at(range
)
296 if not (parent
and parent
.d
) then return end
297 local new_finish
, new_start
298 if parent
.is_list
then
299 local prev
= parent
.before(range
.start
, finishof
, is_comment
)
300 new_finish
= prev
and prev
.finish
+ 1
301 -- XXX: do not skip comments here, so they end up in the second list
302 -- and are not separated from their target expression:
303 local nxt
= new_finish
and parent
.after(new_finish
, startof
)
304 new_start
= nxt
and nxt
.start
306 new_start
= range
.start
307 new_finish
= range
.start
309 if not (new_start
and new_finish
) then return end
310 local opening
= (parent
.p
or '')..parent
.d
311 local closing
= parser
.opposite
[parent
.d
]
312 insert(new_start
, opening
)
313 local sep
= parser
.opposite
[parent
.d
]:find("\n") and "" -- line comments already have a separator
314 or new_finish
== new_start
and " " -- only add a separator if there was none before
316 insert(new_finish
, closing
..sep
)
317 parser
.tree
.rewind(parent
.start
)
318 range
.start
= new_start
+ (parent
.is_list
and 0 or #opening
+ #closing
)
319 range
.finish
= range
.start
320 local _
, parentng
= walker
.sexp_at(range
, true)
321 local _
, grandparent
= walker
.sexp_at(parentng
, true)
322 return reindent_at(grandparent
, range
) or range
.start
325 local function join_sexps(range
)
326 local node
, parent
= walker
.sexp_at(range
, true)
327 local first
= node
and node
.finish
+ 1 == range
.start
and node
or parent
.before(range
.start
, finishof
)
328 local second
= first
~= node
and node
or parent
.after(range
.start
, startof
)
329 if not (first
and second
and first
.d
and
330 (first
.d
== second
.d
or
331 -- join line comments even when their delimiters differ slightly
332 -- (different number of semicolons, existence/lack of a space after them)
333 parser
.opposite
[first
.d
] == parser
.opposite
[second
.d
])) then
336 local opening
= (second
.p
or '')..second
.d
337 local closing
= parser
.opposite
[first
.d
]
339 if not first
.is_list
then
340 pos
= first
.finish
+ 1 - #closing
341 delete(pos
, second
.start
+ #opening
- pos
)
343 delete(second
.start
, #opening
)
344 delete(first
.finish
, #closing
)
345 pos
= second
.start
- #closing
347 parser
.tree
.rewind(first
.start
)
349 range
.finish
= range
.start
350 local _
, parentng
= walker
.sexp_at(range
, true)
351 local _
, grandparent
= walker
.sexp_at(parentng
, true)
352 return reindent_at(grandparent
, range
) or range
.start
355 local function delete_splicing(range
, pos
, splicing
, delete_and_yank
)
356 local action
= {kill
= true, wrap
= splicing
, splice
= splicing
, func
= delete_and_yank
}
357 local sexp
, parent
, n
= walker
.sexp_at(range
, true)
358 local ndeleted
, spliced
= pick_out(range
, pos
, action
)
359 local closing
= spliced
and parser
.opposite
[sexp
.d
]
360 local inner_list_len
= spliced
and sexp
.finish
- sexp
.start
+ 1 - #sexp
.d
- #closing
361 local range_len
= range
.finish
- range
.start
362 local backwards
= pos
== range
.finish
363 local whole_object
= sexp
and
364 (sexp
.start
== range
.start
and sexp
.finish
+ 1 == range
.finish
365 or spliced
and (inner_list_len
<= (backwards
and range_len
+ ndeleted
or range_len
)))
366 local in_head_atom
= sexp
and not sexp
.d
and n
== 1 and #parent
> 1
367 local in_whitespace
= not sexp
368 if whole_object
or in_whitespace
or in_head_atom
then
369 local cur
= whole_object
and sexp
.start
or range
.start
370 parser
.tree
.rewind(cur
)
371 local r
= {start
= cur
, finish
= cur
}
372 local _
, parentng
= walker
.sexp_at(r
, true)
373 local newpos
= reindent_at(parentng
, r
)
376 return not backwards
and ndeleted
<= 0 and range
.finish
- ndeleted
or range
.start
379 local function transpose(range
, first
, second
)
380 if not (first
and second
) then return end
381 local copy1
= first
.text
382 local copy2
= second
.text
383 delete(second
.start
, second
.finish
+ 1 - second
.start
)
384 insert(second
.start
, copy1
)
385 delete(first
.start
, first
.finish
+ 1 - first
.start
)
386 insert(first
.start
, copy2
)
387 parser
.tree
.rewind(first
.start
)
388 range
.start
= second
.finish
+ 1
389 range
.finish
= range
.start
390 local _
, parentng
= walker
.sexp_at(range
, true)
391 return reindent_at(parentng
, range
) or range
.start
394 local function transpose_sexps(range
)
395 local node
, parent
= walker
.sexp_at(range
, true)
396 local first
= node
and node
.finish
+ 1 == range
.start
and node
or parent
.before(range
.start
, finishof
)
397 local second
= first
~= node
and node
or parent
.after(range
.start
, startof
)
398 return transpose(range
, first
, second
)
401 local function transpose_words(range
)
402 local _
, first
= walker
.start_float_before(range
)
403 local _
, second
= walker
.finish_float_after(range
)
404 if first
and second
and first
.start
== second
.start
then
405 _
, first
= walker
.start_float_before(first
)
407 return transpose(range
, first
, second
)
410 local function transpose_chars(range
)
411 local node
, parent
, i
= walker
.sexp_at(range
)
412 local nxt
= i
and parent
[i
+ 1]
413 local pstart
= not parent
.is_root
and parent
.start
+ (parent
.p
and #parent
.p
or 0) + #parent
.d
414 local pfinish
= not parent
.is_root
and parent
.finish
415 local npref
= node
and node
.p
and node
.start
+ #node
.p
416 -- only allow transposing while inside atoms/words and prefixes
417 if node
and ((npref
and (range
.start
<= npref
and range
.start
> node
.start
) or
418 node
.d
and range
.start
> node
.finish
+ 1) or not node
.d
and (not pstart
or range
.start
> pstart
)) then
419 local start
= range
.start
-
420 ((range
.start
== pfinish
or range
.start
== npref
or
421 range
.start
== node
.finish
+ 1 and (parent
.is_list
or parent
.is_root
) and (not nxt
or nxt
.indent
)) and 1 or 0)
422 local str_start
= start
- node
.start
+ 1
423 local char
= node
.text
:sub(str_start
, str_start
)
425 insert(start
- 1, #char
> 0 and char
or " ")
426 parser
.tree
.rewind(start
)
427 local in_head_atom
= i
== 1 and #parent
> 1
428 return parent
.is_list
and in_head_atom
429 and reindent_at(parent
, {start
= start
+ 1, finish
= start
+ 1})
434 local function delete_nonsplicing(range
, pos
, delete_maybe_yank
)
435 local action
= {kill
= true, wrap
= false, splice
= false, func
= delete_maybe_yank
}
436 local ndeleted
, spliced
, opening
= pick_out(range
, pos
, action
)
437 local backwards
= pos
== range
.finish
440 return pos
- ndeleted
442 if ndeleted
== 0 then
443 local closing
= parser
.opposite
[opening
]
444 if closing
:find("\n") then
445 local sexp
= walker
.sexp_at(range
)
446 if pos
== sexp
.start
+ #sexp
.d
then
447 return join_sexps(range
) or pos
448 elseif pos
== sexp
.finish
then
449 local r
= {start
= pos
+ #closing
, finish
= pos
+ #closing
}
450 return join_sexps(r
) or pos
+ #closing
454 return backwards
and (range
.finish
- ndeleted
) or range
.start
457 return backwards
and range
.start
or (range
.finish
- ndeleted
)
461 local function make_wrap(kind
)
462 return function(range
, pos
, auto_square
)
465 local node
, parent
= walker
.sexp_at(range
, true)
466 if parent
.is_root
then
467 -- FIXME: don't do that at the end of a defun
470 local nxt
= parent
.after(range
.finish
, startof
)
471 if not (nxt
and nxt
.indent
and node
and (node
.finish
+ 1 <= range
.start
)) then
476 local function _wrap(r
)
477 local indices
, nodes
= walker
.sexp_path(r
)
478 local delimiter
, closing
= opening
, parser
.opposite
[opening
]
479 local squarewords
= fmt
.squarewords
480 if squarewords
and (auto_square
or squarewords
.not_optional
)
481 and not fmt
:rewrite_bracket_p(indices
, nodes
, range
) then
482 delimiter
, closing
= "[", "]"
484 insert(r
.finish
, closing
)
485 insert(r
.start
, delimiter
)
487 local action
= {kill
= false, wrap
= false, splice
= false, func
= _wrap
}
488 pick_out(range
, pos
, action
)
489 parser
.tree
.rewind(range
.start
)
490 local _
, parentng
= walker
.sexp_at(range
, true)
491 range
.start
= range
.start
+ #opening
492 return reindent_at(parentng
, range
) or range
.start
497 delete_splicing
= delete_splicing
,
498 delete_nonsplicing
= delete_nonsplicing
,
499 reindent_at
= reindent_at
,
501 raise_sexp
= raise_sexp
,
502 slurp_sexp
= slurp_sexp
,
503 barf_sexp
= barf_sexp
,
504 splice_sexp
= splice_sexp
,
505 wrap_round
= make_wrap("("),
506 meta_doublequote
= make_wrap('"'),
507 comment_dwim
= make_wrap(";"),
508 cycle_wrap
= cycle_wrap
,
509 split_sexp
= split_sexp
,
510 join_sexps
= join_sexps
,
511 transpose_sexps
= transpose_sexps
,
512 transpose_words
= transpose_words
,
513 transpose_chars
= transpose_chars
,