allow other line comment opening delimiters
[lisp-parkour.git] / walker.lua
blob30d37d47b99e76dc201b1a339dd83afd9e9478e1
1 local M = {}
3 local function finishof(node) return node.finish end
4 local function startof(node) return node.start end
5 local function is_comment(node) return node.is_comment end
7 function M.new(parser, eol_at, bol_at)
9 local function in_quasilist(t, range)
10 return t and t.d and not t.is_list and
11 range.start >= t.start + #t.d and range.finish <= t.finish + 1 - #parser.opposite[t.d]
12 end
14 local function sexp_at(range, true_sexp)
15 local node, parent, nth = parser.tree.sexp_at(range)
16 if not in_quasilist(node, range) or true_sexp then
17 return node, parent, nth
18 else
19 return node.word_at(range), node
20 end
21 end
23 local function escaped_at(range)
24 local node = parser.escaped.around(range)
25 -- TODO: distinguish between doublequoted strings, chars, line comments, and block comments
26 -- and use approprite offset comparisons for each
27 return node and range.start > node.start and range.finish <= node.finish and node
28 end
30 local function find_before_innermost(t, range, key, pred)
31 if t.is_root or (t.d and t.start < range.start) then
32 if t.d and not t.is_list then
33 local keypos = key(t)
34 local start = keypos == startof(t)
35 local finish = keypos == finishof(t)
36 local newpos
37 if finish then
38 newpos = t.finish_before(range.finish)
39 elseif start then
40 newpos = t.start_before(range.start)
41 end
42 if newpos then
43 local word = t.word_at({start = newpos, finish = newpos})
44 if word and pred(word) then
45 return word
46 end
47 end
48 return
49 end
50 local _, nearest_n = t.before(range.start, startof)
51 if nearest_n then
52 for n = nearest_n, 1, -1 do
53 local child = t[n]
54 if child.d and not child.is_empty then
55 local ret = find_before_innermost(child, range, key, pred)
56 if ret then
57 return ret
58 end
59 elseif key(child) < key(range) and pred(child, n, #t) then
60 return child, n, #t
61 end
62 end
63 end
64 if not t.is_root and pred(t) then
65 return t
66 end
67 end
68 end
70 local function find_after_innermost(t, range, key, pred)
71 if t.is_root or (t.d and t.finish >= range.finish) then
72 if t.d and not t.is_list then
73 local keypos = key(t)
74 local start = keypos == startof(t)
75 local finish = keypos == finishof(t)
76 local newpos
77 if finish then
78 newpos = t.finish_after(math.max(t.start + #t.d, range.finish))
79 elseif start then
80 newpos = range.finish < t.start and t.start + #t.d or t.start_after(range.start)
81 end
82 if newpos then
83 local word = t.word_at({start = newpos, finish = newpos})
84 if word and pred(word) then
85 return word
86 end
87 end
88 return
89 end
90 local _, nearest_n = t.after(range.finish, finishof)
91 if nearest_n then
92 for n = nearest_n, #t do
93 local child = t[n]
94 if child.d and not child.is_empty then
95 local ret = find_after_innermost(child, range, key, pred)
96 if ret then
97 return ret
98 end
99 elseif key(child) >= key(range) and pred(child, n, #t) then
100 return child, n, #t
104 if not t.is_root and pred(t) then
105 return t
110 return {
112 start_before = function(range, skip)
113 local _, parent = parser.tree.sexp_at(range)
114 local node = parent.before(range.start, startof, skip)
115 return node and startof(node)
116 end,
118 start_after = function(range, skip)
119 local _, parent = parser.tree.sexp_at(range)
120 local node = parent.after(range.finish, startof, skip)
121 return node and startof(node)
122 end,
124 finish_before = function(range, skip)
125 local _, parent = parser.tree.sexp_at(range)
126 local node = parent.before(range.start, finishof, skip)
127 return node and finishof(node) + 1
128 end,
130 finish_after = function(range, skip)
131 local _, parent = parser.tree.sexp_at(range)
132 local node = parent.after(range.finish, finishof, skip)
133 return node and finishof(node) + 1
134 end,
136 start_down_after = function(range, skip)
137 local node, parent = parser.tree.sexp_at(range)
138 if in_quasilist(node, range) then return end
139 if node and node.p then
140 -- XXX: if we are past a prefix start, this will prevent skipping over its list
141 range.start = node.start
143 local next_list = parent.find_after(range, function(t) return t.is_list end)
144 if next_list then
145 local first = next_list.after(range.finish, startof, skip)
146 if first then
147 return first.start
148 else
149 return next_list.start + (next_list.p and #next_list.p or 0) + 1
152 end,
154 start_up_before = function(range)
155 local _, parent = sexp_at(range)
156 return parent.d and parent.start
157 end,
159 finish_down_before = function(range, skip)
160 local node, parent = parser.tree.sexp_at(range)
161 if in_quasilist(node, range) then return end
162 local prev_list = parent.find_before(range, function(t) return t.is_list end)
163 if prev_list then
164 local last = prev_list.before(range.start, finishof, skip)
165 if last then
166 return (last.finish or last.p and last.start + #last.p) + 1, prev_list
167 else
168 return prev_list.finish, prev_list
171 end,
173 finish_up_after = function(range)
174 local _, parent = sexp_at(range)
175 return parent.d and parent.finish + 1
176 end,
178 indented_before = function(range)
179 return find_before_innermost(parser.tree, range, startof, function(t) return t.indent end)
180 end,
182 start_float_before = function(range, up_empty_list)
183 if up_empty_list then
184 local _, parent = sexp_at(range)
185 if parent.is_empty then
186 return parent.start, parent
189 local node = find_before_innermost(parser.tree, range, startof, function(t, n, _)
190 return not t.d or t.is_empty and n == 1 end)
191 return node and node.start, node
192 end,
194 finish_float_after = function(range, up_empty_list)
195 if up_empty_list then
196 local _, parent = sexp_at(range)
197 if parent.is_empty then
198 return parent.finish + 1, parent
201 local node = find_after_innermost(parser.tree, range, finishof, function(t, n, max_n)
202 return not t.d or t.is_empty and n == max_n end)
203 return node and node.finish + 1, node
204 end,
206 start_word_after = function(t, range)
207 local newpos = t.start_after(range.start)
208 if newpos then
209 local word = t.word_at({start = newpos, finish = newpos})
210 return word and word.start
211 else
212 local _, parent, n = sexp_at(range, true)
213 local cur, nxt = parent[n], parent[n + 1]
214 if nxt and cur.is_line_comment and nxt.is_line_comment then
215 return nxt.start_after(range.start)
218 end,
220 finish_word_after = function(t, range)
221 local newpos = t.finish_after(math.max(t.start + #t.d, range.finish))
222 if newpos then
223 local word = t.word_at({start = newpos, finish = newpos})
224 return word and word.finish + 1
225 else
226 local _, parent, n = sexp_at(range, true)
227 local cur, nxt = parent[n], parent[n + 1]
228 if nxt and cur.is_line_comment and nxt.is_line_comment then
229 return nxt.finish_after(range.start)
232 end,
234 start_word_before = function(t, range)
235 local newpos = t.start_before(range.start)
236 if newpos then
237 local word = t.word_at({start = newpos, finish = newpos})
238 return word and word.start
239 else
240 local _, parent, n = sexp_at(range, true)
241 local cur, prev = parent[n], parent[n - 1]
242 if prev and cur.is_line_comment and prev.is_line_comment then
243 return prev.start_before(range.start)
246 end,
248 finish_word_before = function(t, range)
249 local newpos = t.finish_before(range.finish)
250 if newpos then
251 local word = t.word_at({start = newpos, finish = newpos})
252 return word and word.finish + 1
253 else
254 local _, parent, n = sexp_at(range, true)
255 local cur, prev = parent[n], parent[n - 1]
256 if prev and cur.is_line_comment and prev.is_line_comment then
257 return prev.finish_before(range.start)
260 end,
262 prev_start_wrapped = function(range)
263 local node, parent = parser.tree.sexp_at(range)
264 if in_quasilist(node, range) then parent = node end
265 local pstart = parent.start and (parent.start + (parent.p and #parent.p or 0) + #parent.d)
266 local bol = bol_at(range.start)
267 local prev_wrapped = (parent.is_list or parent.is_root) and parent.find_before(range,
268 function(t) return t.start < bol and t.finish > bol end,
269 function(t) return t.finish < bol
270 end)
271 local prev_start = prev_wrapped and prev_wrapped.start or bol
272 pstart = pstart or prev_start
273 return math.max(pstart, prev_start)
274 end,
276 next_finish_wrapped = function(range)
277 local node, parent = parser.tree.sexp_at(range)
278 if in_quasilist(node, range) then parent = node end
279 local pfinish = parent.finish and parent.finish + 1 - #parser.opposite[parent.d]
280 local eol = eol_at(range.start)
281 local next_wrapped = (parent.is_list or parent.is_root) and parent.find_after(range,
282 function(t) return t.start < eol and t.finish > eol end,
283 function(t) return t.start > eol
284 end)
285 local next_finish = next_wrapped and next_wrapped.finish + 1 or eol
286 -- XXX: second return value, to be used for safe line-commenting:
287 local next_start_on_line = math.min(pfinish or range.start, next_wrapped and next_wrapped.start or eol)
288 return math.min(pfinish or next_finish, next_finish), next_start_on_line < eol and next_start_on_line or nil
289 end,
291 paragraph_start_before = function(range, prev_line)
292 local prev, n = parser.tree.before(range.start, startof, is_comment)
293 if not prev_line then
294 return prev and prev.start
296 local pprev = n and parser.tree[n - 1]
297 local has_eol = pprev and pprev.is_line_comment
298 local adjacent = pprev and prev and prev.start - (pprev.finish + (has_eol and 0 or 1)) <= 1
299 return prev and prev.start - ((adjacent or prev.start == 0) and 0 or 1)
300 end,
302 paragraph_finish_after = function(range, next_line)
303 local node = parser.tree.around(range)
304 local nxt, n = parser.tree.find_after(node or range, function(t)
305 return not t.is_comment and t.finish > range.finish
306 end)
307 return nxt and nxt.finish + 1 + (next_line and parser.tree[n + 1] and parser.tree[n + 1].indent and 1 or 0)
308 end,
310 beginning_of_quasilist = function(range)
311 local _, parent = sexp_at(range)
312 return parent.start and (parent.start + (parent.p and #parent.p or 0) + #parent.d)
313 end,
315 end_of_quasilist = function(range)
316 local _, parent = sexp_at(range)
317 return parent.finish and parent.finish + 1 - #parser.opposite[parent.d]
318 end,
320 repl_line_begin = function(range)
321 local _, parent = parser.tree.sexp_at(range)
322 if not parent.is_root then return end
323 local prev_indent = parent.find_before(range, function(t) return t.indent end)
324 return prev_indent and prev_indent.start
325 end,
327 wider_than = function(range)
328 local node, parent = parser.tree.sexp_at(range)
329 if not node and parent.is_root then return end
330 local pstart, pfinish
331 if in_quasilist(node, range) then
332 parent = node
333 node = node.word_at(range)
335 if not parent.is_root then
336 pstart, pfinish = parent.start + (parent.p and #parent.p or 0) + #parent.d,
337 parent.finish + 1 - #parser.opposite[parent.d]
339 local same_as_inner = range.start == pstart and range.finish == pfinish
340 local same_as_node = node and range.start == node.start and range.finish - 1 == node.finish
341 if node and not same_as_node then
342 return node.start, node.finish + 1
343 elseif not same_as_inner then
344 return pstart, pfinish
346 return parent.start, parent.finish + 1
347 end,
349 -- opening: nil - bracketed list, false - any quasilist
350 quasilist_at = function(range, opening)
351 local lcd = parser.opposite["\n"]
352 if opening and opening:match('[%' .. lcd .. '%"]') then
353 local escaped = escaped_at(range)
354 return escaped and escaped.d:find("^"..opening) and escaped
355 else
356 local _, nodes = parser.tree.sexp_path(range)
357 local parent
358 local up = #nodes - 1
359 repeat
360 parent = nodes[up]
361 up = up - 1
362 until not parent or (opening == false or not opening or parent.d == opening)
363 return parent
365 end,
367 sexp_at = sexp_at,
368 escaped_at = escaped_at,
369 paragraph_at = parser.tree.around,
370 goto_path = parser.tree.goto_path,
371 sexp_path = parser.tree.sexp_path,
372 find_after = parser.tree.find_after,
373 find_before = parser.tree.find_before,
377 return M