consolidate dialect-specific configuration
[lisp-parkour.git] / input.lua
blobc67e07ae2aca11d226be99aefda853c90373e5cb
1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
4 local M = {}
6 local function startof(node) return node.start end
7 local function finishof(node) return node.finish end
9 local function make_insert(parser, d1, D2, walker, edit, write, eol_at, bol_at)
11 local function open(range, seek, opening, auto_square)
12 local escaped = walker.escaped_at(range)
13 if escaped and range.start > escaped.start then
14 if opening == escaped.d then
15 seek(escaped.finish + 1)
16 return true
17 elseif escaped.is_char then
18 return true
19 end
20 write(range.start, opening)
21 seek(range.start + #opening)
22 parser.tree.rewind(escaped.start)
23 else
24 local _, parent = walker.sexp_at(range)
25 -- TODO: make it work even when before the first existing element on the electric line:
26 local on_electric_line = parent.finish and parent.finish > eol_at(range.finish)
27 and not parent.find_after(range, function(t) return t.indent end)
28 local newpos = edit.insert_pair(range, opening, auto_square)
29 seek(edit.refmt_at(parent, {start = newpos, finish = newpos}, on_electric_line))
30 end
31 return true
32 end
34 local function close(range, seek, kind)
35 local pos = range.start
36 local escaped = walker.escaped_at(range)
37 if escaped then
38 write(pos, kind)
39 seek(pos + 1)
40 parser.tree.rewind(escaped.start)
41 else
42 local _, parent = walker.sexp_at(range)
43 if parent and parent.is_list then
44 range = {start = parent.finish, finish = parent.finish}
45 local newpos = edit.refmt_at(parent, range)
46 seek(newpos + 1)
47 parser.tree.rewind(parent.start)
48 end
49 end
50 return true
51 end
53 local function spacekey(range, seek, char)
54 local sexp, parent = walker.sexp_at(range)
55 -- if parent[#parent + 1] is nil, we are at EOF
56 if not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
57 parser.tree.rewind(parent.start or range.start)
58 end
59 if not parent.is_list or parent.is_empty then return end
60 local after_last = parent[#parent].finish and range.start > parent[#parent].finish
61 local bol = bol_at(range.start)
62 local eol = eol_at(range.start)
63 local prev = parent.before(range.start, finishof)
64 local nxt = parent.after(range.start, startof)
65 local on_empty_line = not sexp and ((prev and prev.finish or parent.start) < bol)
66 and ((nxt and nxt.start or parent.finish) > eol)
67 local at_eol = range.start == eol
68 local newpos
69 if not on_empty_line then
70 local _
71 write(range.start, char)
72 newpos = range.start + #char
73 _, parent = walker.sexp_at(range)
74 else
75 newpos = bol - 1
76 end
77 if not at_eol or on_empty_line then
78 newpos = edit.refmt_at(parent, {start = newpos, finish = newpos}, after_last and not on_empty_line)
79 if on_empty_line then
80 parser.tree.rewind(newpos)
81 write(newpos, char)
82 newpos = newpos + #char
83 end
84 end
85 seek(newpos)
86 return true
87 end
89 local function enterkey(range, seek)
90 local _, parent = walker.sexp_at(range)
91 local newpos = edit.newline(parent, range)
92 if newpos then
93 seek(newpos)
94 return true
95 end
96 end
98 local comment_start = parser.opposite["\n"]
100 local function open_comment(range, seek, char)
101 local escaped = walker.escaped_at(range)
102 if escaped and escaped.is_char then return true end
103 local datum, sexp, parent, descend_into_next_comment, stay_out
104 if not (escaped and range.start > escaped.start) then
105 sexp, parent = walker.sexp_at(range, true)
106 datum = sexp and sexp.p and sexp.p:find("^#") and range.start == sexp.start + 1
107 local _, next_safe = walker.next_finish_wrapped(range)
108 local eol = eol_at(range.start) -- XXX: must be before the write. The vis eol_at wrapper is leaky
109 local bol = bol_at(range.start) -- XXX: must be before the write. The vis eol_at wrapper is leaky
110 if next_safe and not datum then
111 write(next_safe, parser.opposite[char])
113 if not datum then
114 local prev = parent.before(range.start, finishof)
115 local nxt = parent.after(range.start, startof)
116 local pstart = parent.start or 0
117 local no_sexp_to_the_left =
118 not prev and (pstart < bol or parent.is_root) or
119 sexp and (sexp.indent and range.start == sexp.start or sexp.is_line_comment) or
120 not sexp and prev and prev.finish < bol
121 local comment_out_at =
122 (not sexp or sexp.is_line_comment) and nxt and nxt.finish < eol and nxt.start or
123 sexp and range.start <= sexp.finish and sexp.finish < eol and range.start
124 if no_sexp_to_the_left then
125 if sexp and sexp.is_line_comment and range.start == sexp.start then
126 descend_into_next_comment = sexp.start + #sexp.d
127 else
128 if nxt and nxt.is_line_comment and nxt.start < eol then
129 descend_into_next_comment = nxt.start + #nxt.d
130 else
131 char = char:rep(parent.is_root and 3 or 2) .. " "
134 else
135 if (not sexp or range.start == sexp.finish + 1) and
136 nxt and nxt.is_line_comment and not nxt.indent then
137 descend_into_next_comment = nxt.start + #nxt.d
138 else
139 local comment_column = 40
140 local last_col = range.start - bol + 1
141 local padding = math.max(1, comment_column - last_col)
142 char = string.rep(" ", padding)..char
145 if not descend_into_next_comment and comment_out_at then
146 stay_out = comment_out_at
150 if stay_out then
151 range.start = stay_out
153 if not descend_into_next_comment then
154 write(range.start, char)
156 -- if parent[#parent + 1] is nil, we are at EOF
157 if not parent or not parent.is_root or parent.is_parsed(range.start) or parent[#parent + 1] then
158 parser.tree.rewind(range.start - (datum and 1 or 0))
160 if descend_into_next_comment then
161 range.start = descend_into_next_comment
162 elseif not stay_out then
163 range.start = range.start + #char
165 range.finish = range.start
166 if not descend_into_next_comment and not escaped and not parent.is_root then
167 range.start = edit.refmt_at(parent, range)
169 seek(range.start)
170 return true
173 return function(range, seek, char, auto_square)
174 local handler =
175 d1:match(char) and open or
176 char == '"' and open or
177 parser.opposite["|"] and char == "|" and open or
178 D2:match(char) and close or
179 char == " " and spacekey or
180 char == "\n" and enterkey or
181 char == comment_start and open_comment
182 if handler then
183 return handler(range, seek, char, auto_square)
184 else
185 if parser.tree.is_parsed(range.start) then
186 parser.tree.rewind(range.start)
192 function M.new(parser, d1, D2, walker, edit, write, eol_at, bol_at)
193 return {
194 insert = make_insert(parser, d1, D2, walker, edit, write, eol_at, bol_at),
198 return M