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
, 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") -- grep -n XYZZ\Y *.lua
42 local off
= is_last
and parent
.finish
- (has_eol
and 1 or 0) or nxt
.start
- nxt
.indent
- 1
43 local finish
= s
.finish
+ (has_eol
and 0 or 1)
44 normalize_spacing(finish
, off
- finish
, deltas
)
47 delta
= base_indent
- s
.indent
48 if parent
.last_distinguished
then
50 if i
- 1 <= parent
.last_distinguished
then
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
58 if i
> 2 and not parent
[2].indent
then
59 delta
= base_indent
+ parent
[2].start
- pstart
- s
.indent
62 if delta
< 0 then delta
= math
.max(-s
.indent
, delta
) end
64 table.insert(deltas
, {s
.start
, delta
})
69 local nearest_indented
= s
.indent
and s
or walker
.indented_before(s
)
70 local parent_column
= nearest_indented
.indent
+ (s
.start
+ (s
.p
and #s
.p
or 0) + #s
.d
) - nearest_indented
.start
71 reindent_list(s
, parent_column
- adj
, deltas
, keep_electric_space
)
77 -- FIXME: when called at the end of another operation, some nested lists do not get reindented
78 local function reindent_at(scope
, range
, keep_electric_space
)
79 if not range
or scope
.is_root
then return end
80 local parent
= walker
.sexp_at(scope
, true)
81 if not (parent
and parent
.is_list
) then return end
82 local _
, parent_at
, m
= walker
.sexp_at(range
, true)
84 _
, m
= parent_at
.after(range
.start
, startof
)
89 local base
= parent_at
[m
] or parent_at
90 local path
= walker
.sexp_path(base
)
91 local after_start
, at_pfinish
93 local s
= parent_at
[#parent_at
]
94 local has_eol
= s
.is_comment
and parser
.opposite
[s
.d
]:find("\n") -- grep -n XYZZ\Y *.lua
95 if range
.start
>= s
.finish
+ 1 + (has_eol
and 1 or 0) then
98 after_start
= range
.start
< base
.start
and 0 or range
.start
- base
.start
101 after_start
= (parent_at
.p
and #parent_at
.p
or 0) + #parent_at
.d
103 local indented_parent
= parent
.indent
and parent
or parent
.is_list
and walker
.indented_before(parent
)
104 local parent_column
= indented_parent
and (indented_parent
.indent
+ parent
.start
- indented_parent
.start
+
105 (parent
.p
and #parent
.p
or 0) + #parent
.d
) or
107 local deltas
= reindent_list(parent
, parent_column
, {}, keep_electric_space
)
108 table.sort(deltas
, function(d1
, d2
) return d1
[1] > d2
[1] end)
109 for _
, pair
in ipairs(deltas
) do
110 local offset
, delta
= unpack(pair
)
112 insert(offset
, string.rep(" ", delta
))
113 elseif delta
< 0 then
114 delete(offset
+ delta
, -delta
)
117 parser
.tree
.rewind(parent
.start
or 0)
119 local sexp
, parentng
= walker
.goto_path(path
)
122 return parentng
.finish
124 return sexp
.start
+ after_start
131 local function splice(pos
, sexps
, skip
, backwards
, action
)
133 local sexp
= walker
.sexp_at(skip
, true)
134 local start
= sexp
.start
+ (sexp
.p
and #sexp
.p
or 0)
135 local tosplice
= action
.splice
or action
.wrap
or sexp
.is_empty
136 local opening
= (sexp
.p
or '')..sexp
.d
137 local closing
= parser
.opposite
[sexp
.d
]
138 local real_start
= tosplice
and sexp
.start
or start
+ #sexp
.d
139 local splice_closing
= not closing
:find("\n") or sexp
.is_empty
140 local real_finish
= sexp
.finish
+ 1 - (tosplice
and splice_closing
and 0 or #closing
)
141 local first
= backwards
and
142 {start
= real_start
, finish
= math
.max(pos
, start
+ #sexp
.d
)} or
143 {start
= real_start
, finish
= sexp
.is_empty
and pos
or start
+ #sexp
.d
}
144 local second
= backwards
and
145 {start
= sexp
.is_empty
and pos
or sexp
.finish
+ 1 - #closing
, finish
= real_finish
} or
146 {start
= math
.min(pos
, sexp
.finish
+ 1 - #closing
), finish
= real_finish
}
151 local ndeleted
= first
.finish
- first
.start
+ second
.finish
- second
.start
153 sexps
.rewind(sexp
.start
)
156 return first
.finish
- first
.start
, spliced
, opening
, closing
159 local function pick_out(range
, pos
, action
)
160 local sexps
= parser
.tree
161 local skips
= sexps
.unbalanced_delimiters(range
)
162 -- handle splice and kill-splice of forms and strings:
164 local sexp
= walker
.sexp_at(skips
[1], true)
165 local backward_splice
= skips
[1].opening
and pos
>= sexp
.start
+ (sexp
.p
and #sexp
.p
or 0) + #sexp
.d
166 and range
.start
>= sexp
.start
167 local forward_splice
= skips
[1].closing
and pos
<= sexp
.finish
+ 1 - #parser
.opposite
[sexp
.d
]
168 and range
.finish
<= sexp
.finish
+ 1
169 if backward_splice
or forward_splice
then
170 return splice(backward_splice
and range
.finish
or range
.start
, sexps
, sexp
, backward_splice
, action
)
173 local node
, parent
= walker
.sexp_at({start
= range
.finish
, finish
= range
.finish
})
174 local drop_eol
= node
and node
.finish
+ 1 == range
.finish
and node
.is_comment
and parser
.opposite
[node
.d
]:find("\n")
175 table.sort(skips
, function(a
, b
) return a
.start
< b
.start
end)
176 table.insert(skips
, {start
= range
.finish
- (drop_eol
and 1 or 0)}) -- grep -n XYZZ\Y *.lua
177 table.insert(skips
, 1, {finish
= range
.start
})
178 local ndeleted
= drop_eol
and 1 or 0
179 for i
= #skips
- 1, 1, -1 do
180 local region
= {start
= skips
[i
].finish
, finish
= skips
[i
+ 1].start
}
181 if skips
[i
].closing
and skips
[i
+ 1].opening
then
182 -- leave out some of the space between adjacent lists
183 local _
, rparent
= walker
.sexp_at(region
)
184 local nxt
= rparent
.after(region
.start
, startof
)
185 region
.start
= nxt
and nxt
.start
or region
.start
189 ndeleted
= ndeleted
+ (region
.finish
- region
.start
)
192 -- if parent[#parent + 1] is nil, we are at EOF
193 if ndeleted
> 0 and (not parent
.is_root
or parent
[#parent
+ 1]) then
194 sexps
.rewind(range
.start
)
199 local function raise_sexp(range
, pos
)
200 local sexp
, parent
= walker
.sexp_at(range
, true)
201 if sexp
and parent
and parent
.is_list
then
202 delete(sexp
.finish
+ 1, parent
.finish
- sexp
.finish
)
203 delete(parent
.start
, sexp
.start
- parent
.start
)
204 parser
.tree
.rewind(parent
.start
)
205 range
.start
= parent
.start
+ pos
- sexp
.start
206 range
.finish
= range
.start
207 local _
, parentng
= walker
.sexp_at(range
, true)
208 local _
, grandparent
= walker
.sexp_at(parentng
, true)
209 return grandparent
and reindent_at(grandparent
, range
) or range
.start
213 local function slurp_sexp(range
, forward
)
214 local _
, parent
= walker
.sexp_at(range
, true)
215 local seeker
= forward
and walker
.finish_after
or walker
.start_before
216 if not parent
or not parent
.is_list
then return range
.start
end
217 local r
= {start
= parent
.start
, finish
= parent
.finish
+ 1}
218 local newpos
= seeker(r
, is_comment
)
219 if not newpos
then return range
.start
end
220 local opening
= (parent
.p
or '')..parent
.d
221 local closing
= parser
.opposite
[parent
.d
]
222 local delimiter
= forward
and closing
or opening
224 insert(newpos
, delimiter
)
226 delete(forward
and parent
.finish
or parent
.start
, #delimiter
)
228 insert(newpos
, delimiter
)
230 parser
.tree
.rewind(math
.min(parent
.start
, newpos
))
231 local _
, parentng
= walker
.sexp_at(range
, true)
232 return reindent_at(parentng
, range
)
235 local function barf_sexp(range
, forward
)
236 local _
, parent
, m
= walker
.sexp_at(range
, true)
237 local seeker
= forward
and walker
.finish_before
or walker
.start_after
238 -- TODO: barfing out of strings requires calling the parser on them
239 if not parent
or not parent
.is_list
then return range
.start
end
240 local opening
= (parent
.p
or '')..parent
.d
241 local pstart
= parent
.start
+ #opening
242 local r
= {start
= forward
and parent
.finish
- 1 or pstart
, finish
= forward
and parent
.finish
or pstart
+ 1}
243 local newpos
= seeker(r
, is_comment
) or forward
and pstart
or parent
.finish
244 if not newpos
then return range
.start
end
245 local closing
= parser
.opposite
[parent
.d
]
246 local delimiter
= forward
and closing
or opening
248 insert(newpos
, delimiter
)
250 delete(forward
and parent
.finish
or parent
.start
, #delimiter
)
252 insert(newpos
, delimiter
)
254 parser
.tree
.rewind(math
.min(parent
.start
, newpos
))
255 range
.start
= range
.start
+ #delimiter
*
256 (m
== 1 and not forward
and -1 or m
== #parent
and forward
and 1 or 0)
257 local _
, parentng
= walker
.sexp_at(range
, true)
258 local _
, grandparent
= walker
.sexp_at(parentng
, true)
259 return reindent_at(grandparent
, range
) or range
.start
262 local function splice_sexp(range
, _
)
263 local _
, parent
= walker
.sexp_at(range
)
264 if not parent
or not parent
.d
then return end
265 local opening
= (parent
.p
or '')..parent
.d
266 local closing
= parser
.opposite
[parent
.d
]
267 local finish
= parent
.finish
+ 1 - #closing
268 if not closing
:find("\n") then
269 delete(finish
, #closing
)
271 -- TODO: (un)escape special characters, if necessary
272 delete(parent
.start
, parent
.is_empty
and (finish
- parent
.start
) or #opening
)
273 parser
.tree
.rewind(parent
.start
)
274 range
.start
= range
.start
- #opening
275 range
.finish
= range
.start
276 local _
, parentng
= walker
.sexp_at(range
, true)
277 return reindent_at(parentng
, range
)
280 local function cycle_wrap(range
, pos
)
281 local _
, parent
= walker
.sexp_at(range
)
282 if not parent
or not parent
.is_list
then return end
283 local pstart
= parent
.start
+ #((parent
.p
or '')..parent
.d
) - 1
284 local next_kind
= {["("] = "[", ["["] = "{", ["{"] = "("}
285 delete(parent
.finish
, 1)
286 insert(parent
.finish
, parser
.opposite
[next_kind
[parent
.d]]
)
287 delete(pstart
, #parent
.d
)
288 insert(pstart
, next_kind
[parent
.d
])
289 parser
.tree
.rewind(parent
.start
)
293 local function split_sexp(range
)
294 local _
, parent
= walker
.sexp_at(range
)
295 if not (parent
and parent
.d
) then return end
296 local new_finish
, new_start
297 if parent
.is_list
then
298 local prev
= parent
.before(range
.start
, finishof
, is_comment
)
299 new_finish
= prev
and prev
.finish
+ 1
300 -- XXX: do not skip comments here, so they end up in the second list
301 -- and are not separated from their target expression:
302 local nxt
= new_finish
and parent
.after(new_finish
, startof
)
303 new_start
= nxt
and nxt
.start
305 new_start
= range
.start
306 new_finish
= range
.start
308 if not (new_start
and new_finish
) then return end
309 local opening
= (parent
.p
or '')..parent
.d
310 local closing
= parser
.opposite
[parent
.d
]
311 insert(new_start
, opening
)
312 local sep
= parser
.opposite
[parent
.d
]:find("\n") and "" -- line comments already have a separator
313 or new_finish
== new_start
and " " -- only add a separator if there was none before
315 insert(new_finish
, closing
..sep
)
316 parser
.tree
.rewind(parent
.start
)
317 range
.start
= new_start
+ (parent
.is_list
and 0 or #opening
+ #closing
)
318 range
.finish
= range
.start
319 local _
, parentng
= walker
.sexp_at(range
, true)
320 local _
, grandparent
= walker
.sexp_at(parentng
, true)
321 return reindent_at(grandparent
, range
) or range
.start
324 local function join_sexps(range
)
325 local node
, parent
= walker
.sexp_at(range
, true)
326 local first
= node
and node
.finish
+ 1 == range
.start
and node
or parent
.before(range
.start
, finishof
)
327 local second
= first
~= node
and node
or parent
.after(range
.start
, startof
)
328 if not (first
and second
and first
.d
and
329 (first
.d
== second
.d
or
330 -- join line comments even when their delimiters differ slightly
331 -- (different number of ;'s, existence/lack of a space after them)
332 first
.d
:find("^;") and second
.d
:find("^;"))) then
335 local opening
= (second
.p
or '')..second
.d
336 local closing
= parser
.opposite
[first
.d
]
338 if not first
.is_list
then
339 pos
= first
.finish
+ 1 - #closing
340 delete(pos
, second
.start
+ #opening
- pos
)
342 delete(second
.start
, #opening
)
343 delete(first
.finish
, #closing
)
344 pos
= second
.start
- #closing
346 parser
.tree
.rewind(first
.start
)
348 range
.finish
= range
.start
349 local _
, parentng
= walker
.sexp_at(range
, true)
350 local _
, grandparent
= walker
.sexp_at(parentng
, true)
351 return reindent_at(grandparent
, range
) or range
.start
354 local function delete_splicing(range
, pos
, splicing
, delete_and_yank
)
355 local action
= {kill
= true, wrap
= splicing
, splice
= splicing
, func
= delete_and_yank
}
356 local sexp
, parent
, n
= walker
.sexp_at(range
, true)
357 local ndeleted
, spliced
= pick_out(range
, pos
, action
)
358 local closing
= spliced
and parser
.opposite
[sexp
.d
]
359 local inner_list_len
= spliced
and sexp
.finish
- sexp
.start
+ 1 - #sexp
.d
- #closing
360 local range_len
= range
.finish
- range
.start
361 local backwards
= pos
== range
.finish
362 local whole_object
= sexp
and
363 (sexp
.start
== range
.start
and sexp
.finish
+ 1 == range
.finish
364 or spliced
and (inner_list_len
<= (backwards
and range_len
+ ndeleted
or range_len
)))
365 local in_head_atom
= sexp
and not sexp
.d
and n
== 1 and #parent
> 1
366 local in_whitespace
= not sexp
367 if whole_object
or in_whitespace
or in_head_atom
then
368 local cur
= whole_object
and sexp
.start
or range
.start
369 parser
.tree
.rewind(cur
)
370 local r
= {start
= cur
, finish
= cur
}
371 local _
, parentng
= walker
.sexp_at(r
, true)
372 local newpos
= reindent_at(parentng
, r
)
375 return not backwards
and ndeleted
<= 0 and range
.finish
- ndeleted
or range
.start
378 local function transpose(range
, first
, second
)
379 if not (first
and second
) then return end
380 local copy1
= first
.text
381 local copy2
= second
.text
382 delete(second
.start
, second
.finish
+ 1 - second
.start
)
383 insert(second
.start
, copy1
)
384 delete(first
.start
, first
.finish
+ 1 - first
.start
)
385 insert(first
.start
, copy2
)
386 parser
.tree
.rewind(first
.start
)
387 range
.start
= second
.finish
+ 1
388 range
.finish
= range
.start
389 local _
, parentng
= walker
.sexp_at(range
, true)
390 return reindent_at(parentng
, range
) or range
.start
393 local function transpose_sexps(range
)
394 local node
, parent
= walker
.sexp_at(range
, true)
395 local first
= node
and node
.finish
+ 1 == range
.start
and node
or parent
.before(range
.start
, finishof
)
396 local second
= first
~= node
and node
or parent
.after(range
.start
, startof
)
397 return transpose(range
, first
, second
)
400 local function transpose_words(range
)
401 local _
, first
= walker
.start_float_before(range
)
402 local _
, second
= walker
.finish_float_after(range
)
403 if first
and second
and first
.start
== second
.start
then
404 _
, first
= walker
.start_float_before(first
)
406 return transpose(range
, first
, second
)
409 local function transpose_chars(range
)
410 local node
, parent
, i
= walker
.sexp_at(range
)
411 local nxt
= i
and parent
[i
+ 1]
412 local pstart
= not parent
.is_root
and parent
.start
+ (parent
.p
and #parent
.p
or 0) + #parent
.d
413 local pfinish
= not parent
.is_root
and parent
.finish
414 local npref
= node
and node
.p
and node
.start
+ #node
.p
415 -- only allow transposing while inside atoms/words and prefixes
416 if node
and ((npref
and (range
.start
<= npref
and range
.start
> node
.start
) or
417 node
.d
and range
.start
> node
.finish
+ 1) or not node
.d
and (not pstart
or range
.start
> pstart
)) then
418 local start
= range
.start
-
419 ((range
.start
== pfinish
or range
.start
== npref
or
420 range
.start
== node
.finish
+ 1 and (parent
.is_list
or parent
.is_root
) and (not nxt
or nxt
.indent
)) and 1 or 0)
421 local str_start
= start
- node
.start
+ 1
422 local char
= node
.text
:sub(str_start
, str_start
)
424 insert(start
- 1, #char
> 0 and char
or " ")
425 parser
.tree
.rewind(start
)
426 local in_head_atom
= i
== 1 and #parent
> 1
427 return parent
.is_list
and in_head_atom
428 and reindent_at(parent
, {start
= start
+ 1, finish
= start
+ 1})
433 local function delete_nonsplicing(range
, pos
, delete_maybe_yank
)
434 local action
= {kill
= true, wrap
= false, splice
= false, func
= delete_maybe_yank
}
435 local ndeleted
, spliced
, opening
= pick_out(range
, pos
, action
)
436 local backwards
= pos
== range
.finish
439 return pos
- ndeleted
441 if ndeleted
== 0 then
442 local sexp
= walker
.sexp_at(range
)
443 if sexp
and sexp
.d
and sexp
.d
:match("^;") then
444 if pos
== sexp
.start
+ #sexp
.d
then
445 return join_sexps(range
)
446 elseif pos
== sexp
.finish
then
447 local closing
= parser
.opposite
[sexp
.d
]
448 local r
= {start
= pos
+ #closing
, finish
= pos
+ #closing
}
449 return join_sexps(r
) or pos
+ #closing
453 return backwards
and (range
.finish
- ndeleted
) or range
.start
456 return backwards
and range
.start
or (range
.finish
- ndeleted
)
460 local function make_wrap(kind
)
461 return function(range
, pos
)
464 local node
, parent
= walker
.sexp_at(range
, true)
465 if parent
.is_root
then
466 -- FIXME: don't do that at the end of a defun
469 local nxt
= parent
.after(range
.finish
, startof
)
470 if not (nxt
and nxt
.indent
and node
and (node
.finish
+ 1 <= range
.start
)) then
475 local function _wrap(r
)
476 insert(r
.finish
, parser
.opposite
[opening
])
477 -- TODO (un)escape special characters, if necessary
478 insert(r
.start
, opening
)
480 local action
= {kill
= false, wrap
= false, splice
= false, func
= _wrap
}
481 pick_out(range
, pos
, action
)
482 parser
.tree
.rewind(range
.start
)
483 local _
, parentng
= walker
.sexp_at(range
, true)
484 range
.start
= range
.start
+ #opening
485 return reindent_at(parentng
, range
) or range
.start
490 delete_splicing
= delete_splicing
,
491 delete_nonsplicing
= delete_nonsplicing
,
492 reindent_at
= reindent_at
,
494 raise_sexp
= raise_sexp
,
495 slurp_sexp
= slurp_sexp
,
496 barf_sexp
= barf_sexp
,
497 splice_sexp
= splice_sexp
,
498 wrap_round
= make_wrap("("),
499 meta_doublequote
= make_wrap('"'),
500 comment_dwim
= make_wrap(";"),
501 cycle_wrap
= cycle_wrap
,
502 split_sexp
= split_sexp
,
503 join_sexps
= join_sexps
,
504 transpose_sexps
= transpose_sexps
,
505 transpose_words
= transpose_words
,
506 transpose_chars
= transpose_chars
,