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
+ (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
and range
.start
>= sexp
.start
166 local forward_splice
= skips
[1].closing
and pos
<= sexp
.finish
+ 1 - #parser
.opposite
[sexp
.d
] and range
.finish
<= sexp
.finish
+ 1
167 if backward_splice
or forward_splice
then
168 return splice(backward_splice
and range
.finish
or range
.start
, sexps
, sexp
, backward_splice
, action
)
171 local node
, parent
= walker
.sexp_at({start
= range
.finish
, finish
= range
.finish
})
172 local drop_eol
= node
and node
.is_comment
and parser
.opposite
[node
.d
]:find("\n")
173 table.sort(skips
, function(a
, b
) return a
.start
< b
.start
end)
174 table.insert(skips
, {start
= range
.finish
- (drop_eol
and 1 or 0)}) -- grep -n XYZZ\Y *.lua
175 table.insert(skips
, 1, {finish
= range
.start
})
176 local ndeleted
= drop_eol
and 1 or 0
177 for i
= #skips
- 1, 1, -1 do
178 local region
= {start
= skips
[i
].finish
, finish
= skips
[i
+ 1].start
}
179 if skips
[i
].closing
and skips
[i
+ 1].opening
then
180 -- leave out some of the space between adjacent lists
181 local _
, rparent
= walker
.sexp_at(region
)
182 local nxt
= rparent
.after(region
.start
, startof
)
183 region
.start
= nxt
and nxt
.start
or region
.start
187 ndeleted
= ndeleted
+ (region
.finish
- region
.start
)
190 -- if parent[#parent + 1] is nil, we are at EOF
191 if ndeleted
> 0 and (not parent
.is_root
or parent
[#parent
+ 1]) then
192 sexps
.rewind(range
.start
)
197 local function raise_sexp(range
, pos
)
198 local sexp
, parent
= walker
.sexp_at(range
, true)
199 if sexp
and parent
and parent
.is_list
then
200 delete(sexp
.finish
+ 1, parent
.finish
- sexp
.finish
)
201 delete(parent
.start
, sexp
.start
- parent
.start
)
202 parser
.tree
.rewind(parent
.start
)
203 range
.start
= parent
.start
+ pos
- sexp
.start
204 range
.finish
= range
.start
205 local _
, parentng
= walker
.sexp_at(range
, true)
206 local _
, grandparent
= walker
.sexp_at(parentng
, true)
207 return grandparent
and reindent_at(grandparent
, range
) or range
.start
211 local function slurp_sexp(range
, forward
)
212 local _
, parent
= walker
.sexp_at(range
, true)
213 local seeker
= forward
and walker
.finish_after
or walker
.start_before
214 if not parent
or not parent
.is_list
then return range
.start
end
215 local r
= {start
= parent
.start
, finish
= parent
.finish
+ 1}
216 local newpos
= seeker(r
, is_comment
)
217 if not newpos
then return range
.start
end
218 local opening
= (parent
.p
or '')..parent
.d
219 local closing
= parser
.opposite
[parent
.d
]
220 local delimiter
= forward
and closing
or opening
222 insert(newpos
, delimiter
)
224 delete(forward
and parent
.finish
or parent
.start
, #delimiter
)
226 insert(newpos
, delimiter
)
228 parser
.tree
.rewind(math
.min(parent
.start
, newpos
))
229 local _
, parentng
= walker
.sexp_at(range
, true)
230 return reindent_at(parentng
, range
)
233 local function barf_sexp(range
, forward
)
234 local _
, parent
, m
= walker
.sexp_at(range
, true)
235 local seeker
= forward
and walker
.finish_before
or walker
.start_after
236 -- TODO: barfing out of strings requires calling the parser on them
237 if not parent
or not parent
.is_list
then return range
.start
end
238 local opening
= (parent
.p
or '')..parent
.d
239 local pstart
= parent
.start
+ #opening
240 local r
= {start
= forward
and parent
.finish
- 1 or pstart
, finish
= forward
and parent
.finish
or pstart
+ 1}
241 local newpos
= seeker(r
, is_comment
) or forward
and pstart
or parent
.finish
242 if not newpos
then return range
.start
end
243 local closing
= parser
.opposite
[parent
.d
]
244 local delimiter
= forward
and closing
or opening
246 insert(newpos
, delimiter
)
248 delete(forward
and parent
.finish
or parent
.start
, #delimiter
)
250 insert(newpos
, delimiter
)
252 parser
.tree
.rewind(math
.min(parent
.start
, newpos
))
253 range
.start
= range
.start
+ #delimiter
*
254 (m
== 1 and not forward
and -1 or m
== #parent
and forward
and 1 or 0)
255 local _
, parentng
= walker
.sexp_at(range
, true)
256 local _
, grandparent
= walker
.sexp_at(parentng
, true)
257 return reindent_at(grandparent
, range
) or range
.start
260 local function splice_sexp(range
, _
)
261 local _
, parent
= walker
.sexp_at(range
)
262 if not parent
or not parent
.d
then return end
263 local opening
= (parent
.p
or '')..parent
.d
264 local closing
= parser
.opposite
[parent
.d
]
265 local finish
= parent
.finish
+ 1 - #closing
266 if not closing
:find("\n") then
267 delete(finish
, #closing
)
269 -- TODO: (un)escape special characters, if necessary
270 delete(parent
.start
, parent
.is_empty
and (finish
- parent
.start
) or #opening
)
271 parser
.tree
.rewind(parent
.start
)
272 range
.start
= range
.start
- #opening
273 range
.finish
= range
.start
274 local _
, parentng
= walker
.sexp_at(range
, true)
275 return reindent_at(parentng
, range
)
278 local function cycle_wrap(range
, pos
)
279 local _
, parent
= walker
.sexp_at(range
)
280 if not parent
or not parent
.is_list
then return end
281 local pstart
= parent
.start
+ #((parent
.p
or '')..parent
.d
) - 1
282 local next_kind
= {["("] = "[", ["["] = "{", ["{"] = "("}
283 delete(parent
.finish
, 1)
284 insert(parent
.finish
, parser
.opposite
[next_kind
[parent
.d]]
)
285 delete(pstart
, #parent
.d
)
286 insert(pstart
, next_kind
[parent
.d
])
287 parser
.tree
.rewind(parent
.start
)
291 local function split_sexp(range
)
292 local _
, parent
= walker
.sexp_at(range
)
293 if not (parent
and parent
.d
) then return end
294 local new_finish
, new_start
295 if parent
.is_list
then
296 local prev
= parent
.before(range
.start
, finishof
, is_comment
)
297 new_finish
= prev
and prev
.finish
+ 1
298 -- XXX: do not skip comments here, so they end up in the second list
299 -- and are not separated from their target expression:
300 local nxt
= new_finish
and parent
.after(new_finish
, startof
)
301 new_start
= nxt
and nxt
.start
303 new_start
= range
.start
304 new_finish
= range
.start
306 if not (new_start
and new_finish
) then return end
307 local opening
= (parent
.p
or '')..parent
.d
308 local closing
= parser
.opposite
[parent
.d
]
309 insert(new_start
, opening
)
310 local sep
= parser
.opposite
[parent
.d
]:find("\n") and "" -- line comments already have a separator
311 or new_finish
== new_start
and " " -- only add a separator if there was none before
313 insert(new_finish
, closing
..sep
)
314 parser
.tree
.rewind(parent
.start
)
315 range
.start
= new_start
+ (parent
.is_list
and 0 or #opening
+ #closing
)
316 range
.finish
= range
.start
317 local _
, parentng
= walker
.sexp_at(range
, true)
318 local _
, grandparent
= walker
.sexp_at(parentng
, true)
319 return reindent_at(grandparent
, range
) or range
.start
322 local function join_sexps(range
)
323 local node
, parent
= walker
.sexp_at(range
, true)
324 local first
= node
and node
.finish
+ 1 == range
.start
and node
or parent
.before(range
.start
, finishof
)
325 local second
= first
~= node
and node
or parent
.after(range
.start
, startof
)
326 if not (first
and second
and first
.d
and
327 (first
.d
== second
.d
or
328 -- join line comments even when their delimiters differ slightly
329 -- (different number of ;'s, existence/lack of a space after them)
330 first
.d
:find("^;") and second
.d
:find("^;"))) then
333 local opening
= (second
.p
or '')..second
.d
334 local closing
= parser
.opposite
[first
.d
]
336 if not first
.is_list
then
337 pos
= first
.finish
+ 1 - #closing
338 delete(pos
, second
.start
+ #opening
- pos
)
340 delete(second
.start
, #opening
)
341 delete(first
.finish
, #closing
)
342 pos
= second
.start
- #closing
344 parser
.tree
.rewind(first
.start
)
346 range
.finish
= range
.start
347 local _
, parentng
= walker
.sexp_at(range
, true)
348 local _
, grandparent
= walker
.sexp_at(parentng
, true)
349 return reindent_at(grandparent
, range
) or range
.start
352 local function transpose(range
, first
, second
)
353 if not (first
and second
) then return end
354 local copy1
= first
.text
355 local copy2
= second
.text
356 delete(second
.start
, second
.finish
+ 1 - second
.start
)
357 insert(second
.start
, copy1
)
358 delete(first
.start
, first
.finish
+ 1 - first
.start
)
359 insert(first
.start
, copy2
)
360 parser
.tree
.rewind(first
.start
)
361 range
.start
= second
.finish
+ 1
362 range
.finish
= range
.start
363 local _
, parentng
= walker
.sexp_at(range
, true)
364 return reindent_at(parentng
, range
) or range
.start
367 local function transpose_sexps(range
)
368 local node
, parent
= walker
.sexp_at(range
, true)
369 local first
= node
and node
.finish
+ 1 == range
.start
and node
or parent
.before(range
.start
, finishof
)
370 local second
= first
~= node
and node
or parent
.after(range
.start
, startof
)
371 return transpose(range
, first
, second
)
374 local function transpose_words(range
)
375 local _
, first
= walker
.start_float_before(range
)
376 local _
, second
= walker
.finish_float_after(range
)
377 if first
and second
and first
.start
== second
.start
then
378 _
, first
= walker
.start_float_before(first
)
380 return transpose(range
, first
, second
)
383 local function transpose_chars(range
)
384 local node
, parent
, i
= walker
.sexp_at(range
)
385 local nxt
= i
and parent
[i
+ 1]
386 local pstart
= not parent
.is_root
and parent
.start
+ (parent
.p
and #parent
.p
or 0) + #parent
.d
387 local pfinish
= not parent
.is_root
and parent
.finish
388 local npref
= node
and node
.p
and node
.start
+ #node
.p
389 -- only allow transposing while inside atoms/words and prefixes
390 if node
and ((npref
and (range
.start
<= npref
and range
.start
> node
.start
) or
391 node
.d
and range
.start
> node
.finish
+ 1) or not node
.d
and (not pstart
or range
.start
> pstart
)) then
392 local start
= range
.start
-
393 ((range
.start
== pfinish
or range
.start
== npref
or
394 range
.start
== node
.finish
+ 1 and (parent
.is_list
or parent
.is_root
) and (not nxt
or nxt
.indent
)) and 1 or 0)
395 local str_start
= start
- node
.start
+ 1
396 local char
= node
.text
:sub(str_start
, str_start
)
398 insert(start
- 1, #char
> 0 and char
or " ")
399 parser
.tree
.rewind(start
)
400 return parent
.is_list
and reindent_at(parent
, {start
= start
+ 1, finish
= start
+ 1}) or start
+ 1
404 local function make_wrap(kind
)
405 return function(range
, pos
)
408 local node
, parent
= walker
.sexp_at(range
, true)
409 if parent
.is_root
then
410 -- FIXME: don't do that at the end of a defun
413 local nxt
= parent
.after(range
.finish
, startof
)
414 if not (nxt
and nxt
.indent
and node
and (node
.finish
+ 1 <= range
.start
)) then
419 local function _wrap(r
)
420 insert(r
.finish
, parser
.opposite
[opening
])
421 -- TODO (un)escape special characters, if necessary
422 insert(r
.start
, opening
)
424 local action
= {kill
= false, wrap
= false, splice
= false, func
= _wrap
}
425 pick_out(range
, pos
, action
)
426 parser
.tree
.rewind(range
.start
)
427 local _
, parentng
= walker
.sexp_at(range
, true)
428 range
.start
= range
.start
+ #opening
429 return reindent_at(parentng
, range
) or range
.start
434 reindent_at
= reindent_at
,
436 raise_sexp
= raise_sexp
,
437 slurp_sexp
= slurp_sexp
,
438 barf_sexp
= barf_sexp
,
439 splice_sexp
= splice_sexp
,
440 wrap_round
= make_wrap("("),
441 meta_doublequote
= make_wrap('"'),
442 comment_dwim
= make_wrap(";"),
443 cycle_wrap
= cycle_wrap
,
444 split_sexp
= split_sexp
,
445 join_sexps
= join_sexps
,
446 transpose_sexps
= transpose_sexps
,
447 transpose_words
= transpose_words
,
448 transpose_chars
= transpose_chars
,