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