migrate to 12.0
[ta-parkour.git] / init.lua
blob717c76522d3c405587f8eb743adb81b84799f239
1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
4 local M = {
5 auto_square_brackets = false,
6 lispwords = {},
7 repl_fifo = nil,
8 emacs = false,
11 local textadept = require"textadept"
12 local events = require"events"
13 local keys = require"keys"
15 local ui = ui
16 local CURSES = CURSES
18 -- copied from core/keys.lua
19 local CTRL, ALT --[[, CMD]], SHIFT = 'ctrl+', not CURSES and 'alt+' or 'meta+' --[[, 'cmd+']], 'shift+'
21 -- XXX: in Lua 5.2 unpack() was moved into table
22 local unpack = table.unpack or unpack
24 local env = {}
26 local cwd = ...
27 local init, supported = unpack((require(cwd..".parkour")))
29 local do_copy = false
30 local initialized = false
31 local orig = {}
33 setmetatable(M.lispwords, {__index = function(t, filetype)
34 if supported[filetype] then
35 local recfg = {__newindex = function(_, k, v)
36 events.connect(events.FILE_OPENED, function()
37 if filetype == buffer.lexer_language then
38 env[buffer].fmt.lispwords[k] = v
39 end
40 end)
41 end}
42 t[filetype] = setmetatable({}, recfg)
43 return t[filetype]
44 end
45 end})
47 local H = {M = {}, T = {}, O = {}, I = {}, A = {}} -- handler functions
48 local B = {M = {}, T = {}, I = {}} -- bindings
49 local new = {} -- constructors
51 local function range_by_pos(pos)
52 return {start = pos, finish = pos}
53 end
55 function H.A.paste_reindent()
56 local clipboard = ui.clipboard_text
57 if not (clipboard and #clipboard > 0) then return end
58 buffer:begin_undo_action()
59 buffer:paste()
60 local pos = buffer.selection_n_caret[1] - 1
61 local cursor = range_by_pos(pos)
62 local _, parent = env[buffer].walker.sexp_at(cursor, true)
63 env[buffer].edit.refmt_at(parent, cursor)
64 buffer:end_undo_action()
65 end
67 local function is_comment(t) return t.is_comment end
69 function H.M.backward(range)
70 return env[buffer].walker.start_before(range, is_comment)
71 end
73 function H.M.forward(range)
74 return env[buffer].walker.finish_after(range, is_comment)
75 end
77 function H.M.forward_up(range)
78 return env[buffer].walker.finish_up_after(range)
79 end
81 function H.M.forward_down(range)
82 return env[buffer].walker.start_down_after(range, is_comment)
83 end
85 function H.M.backward_up(range)
86 return env[buffer].walker.start_up_before(range)
87 end
89 function H.M.backward_down(range)
90 return env[buffer].walker.finish_down_before(range, is_comment)
91 end
93 function H.M.beginning_of_sentence(range)
94 return env[buffer].walker.anylist_start(range)
95 end
97 function H.M.end_of_sentence(range)
98 return env[buffer].walker.anylist_finish(range)
99 end
101 function H.M.next_section(range)
102 local newpos = env[buffer].walker.find_after(range, function(t) return t.section and t.start > range.start end)
103 return newpos and newpos.start or buffer.length
106 function H.M.prev_section(range)
107 local newpos = env[buffer].walker.find_before(range, function(t) return t.section end)
108 return newpos and newpos.start or 0
111 function H.M.beginning_of_defun(range)
112 local sexp = env[buffer].walker.paragraph_at(range)
113 return sexp and sexp.start
116 function H.M.end_of_defun(range)
117 local sexp = env[buffer].walker.paragraph_at(range)
118 return sexp and sexp.finish + 1
121 function H.M.backward_word(range)
122 -- XXX: ignore the second value:
123 return (env[buffer].walker.start_float_before(range, true))
126 function H.M.forward_word(range)
127 -- XXX: ignore the second value:
128 return (env[buffer].walker.finish_float_after(range, true))
131 function H.M.prev_start(range)
132 local escaped = env[buffer].walker.escaped_at(range)
133 local newpos
134 if escaped then
135 newpos = env[buffer].walker.start_word_before(escaped, range)
136 else
137 newpos = env[buffer].walker.start_before(range, is_comment)
139 return newpos
142 function H.M.next_finish(range)
143 local escaped = env[buffer].walker.escaped_at(range)
144 local newpos
145 if escaped then
146 newpos = env[buffer].walker.finish_word_after(escaped, range)
147 else
148 newpos = env[buffer].walker.finish_after(range, is_comment)
150 return newpos
153 local function line_end(range)
154 -- XXX: ignore the second value:
155 return (env[buffer].walker.next_finish_wrapped(range))
158 function H.T.line_end_extend(range)
159 return range.start, line_end(range)
162 local function line_begin(range)
163 return env[buffer].walker.prev_start_wrapped(range)
166 function H.T.home_extend(range)
167 return line_begin(range), range.finish
170 function H.M.back_to_indentation(_, pos)
171 return buffer.line_indent_position[buffer:line_from_position(pos + 1)] - 1
174 function H.T.mark_sexp_backwards(range)
175 local start = H.M.prev_start(range)
176 if start then
177 return start, range.finish
181 function H.T.mark_sexp(range, pos)
182 local backward = pos < range.finish
183 if backward then
184 local start = H.T.mark_sexp_backwards(range)
185 return range.finish, start
187 local finish = H.M.next_finish(range)
188 if finish then
189 return range.start, finish
193 function H.T.paragraph(range)
194 local sexps = env[buffer].walker.repl_line_at(range)
195 if sexps then
196 return sexps.start, sexps.finish + 1
200 function H.T.mark_defun(range)
201 local sexp = env[buffer].walker.paragraph_at(range)
202 if sexp and not sexp.is_comment then
203 return sexp.start, sexp.finish + 1
207 function H.T.expand_region(range)
208 return env[buffer].walker.wider_than(range)
211 local function eval_repl_line(range, pos)
212 local sexps = env[buffer].walker.repl_line_at(range)
213 return sexps and H.O.eval_defun({start = sexps.start, finish = pos}, pos)
216 function H.O.format(range, pos)
217 local cursor = range_by_pos(pos)
218 -- XXX: don't call range_by_pos() from the argument list, as it returns a second value
219 return env[buffer].edit.refmt_at(range, cursor) or pos
222 local function make_yank(handler, fragments)
223 return function(range)
224 local txt = buffer:text_range(range.start + 1, range.finish + 1)
225 if txt then
226 table.insert(fragments, 1, txt)
228 return handler and handler(range)
232 local function _delete(range)
233 return buffer:delete_range(range.start + 1, range.finish - range.start)
236 function H.O.delete(range, pos)
237 local fragments = {}
238 local delete_maybe_yank = do_copy and make_yank(_delete, fragments) or _delete
239 local splicing = true
240 return env[buffer].edit.delete_splicing(range, pos, splicing, delete_maybe_yank), fragments
243 function H.O.change(range, pos)
244 local fragments = {}
245 local delete_maybe_yank = do_copy and make_yank(_delete, fragments) or _delete
246 return env[buffer].edit.delete_nonsplicing(range, pos, delete_maybe_yank), fragments
249 function H.O.yank(range, pos)
250 local fragments = {}
251 local yank = make_yank(nil, fragments)
252 local action = {kill = false, wrap = true, splice = false, func = yank}
253 env[buffer].edit.pick_out(range, pos, action)
254 return range.start, fragments
257 local function backward_char(range)
258 return buffer.position_before(range.start + 1) - 1
261 local function forward_char(range)
262 return buffer.position_after(range.start + 1) - 1
265 function H.O.wrap_round(range, pos)
266 return env[buffer].edit.wrap_round(range, pos, M.auto_square_brackets)
269 function H.O.wrap_square(range, pos)
270 return env[buffer].edit.wrap_square(range, pos, M.auto_square_brackets)
273 function H.O.wrap_curly(range, pos)
274 return env[buffer].edit.wrap_curly(range, pos, M.auto_square_brackets)
277 function H.O.meta_doublequote(range, pos)
278 return env[buffer].edit.meta_doublequote(range, pos, M.auto_square_brackets)
281 function H.O.wrap_comment(range, pos)
282 return env[buffer].edit.wrap_comment(range, pos)
285 function H.O.toggle_comment(range, pos)
286 local sexp = env[buffer].walker.sexp_at(range, true)
287 if sexp and not sexp.is_comment then
288 return env[buffer].edit.wrap_comment({start = sexp.start, finish = sexp.finish + 1}, pos)
289 elseif sexp then
290 return env[buffer].edit.splice_sexp(range, pos)
294 function H.O.close_and_newline(range)
295 return env[buffer].edit.close_and_newline(range)
298 function H.O.open_next_line(range)
299 return env[buffer].edit.close_and_newline(range, "\n")
302 function H.O.raise_sexp(range, pos)
303 return env[buffer].edit.raise_sexp(range, pos) or pos
306 function H.O.splice_sexp(range, pos)
307 return env[buffer].edit.splice_anylist(range, pos)
310 function H.O.cycle_wrap(range, pos)
311 return env[buffer].edit.cycle_wrap(range, pos)
314 function H.O.forward_slurp(range)
315 return env[buffer].edit.slurp_sexp(range, true)
318 function H.O.backward_slurp(range)
319 return env[buffer].edit.slurp_sexp(range)
322 function H.O.forward_barf(range)
323 return env[buffer].edit.barf_sexp(range, true)
326 function H.O.backward_barf(range)
327 return env[buffer].edit.barf_sexp(range)
330 function H.O.split_sexp(range, pos)
331 return env[buffer].edit.split_anylist(range) or pos
334 function H.O.join_sexps(range, pos)
335 return env[buffer].edit.join_anylists(range) or pos
338 function H.O.convolute_sexp(range)
339 return env[buffer].edit.convolute_lists(range)
342 function H.O.transpose_sexps(range, pos)
343 return env[buffer].edit.transpose_sexps(range) or pos
346 function H.O.transpose_words(range, pos)
347 return env[buffer].edit.transpose_words(range) or pos
350 function H.O.transpose_chars(range, pos)
351 return env[buffer].edit.transpose_chars(range) or pos
354 local repl_fifo
356 function H.O.eval_defun(range, pos)
357 if not M.repl_fifo or range.finish == range.start then return pos end
358 if pos > range.finish then return pos end
359 local unbalanced = env[buffer].parser.tree.unbalanced_delimiters(range)
360 if unbalanced and #unbalanced > 0 then return pos end
361 local errmsg
362 if not repl_fifo then
363 repl_fifo, errmsg = io.open(M.repl_fifo, "a+")
365 if repl_fifo then
366 repl_fifo:write(buffer:text_range(range.start + 1, range.finish + 1), "\n")
367 repl_fifo:flush()
368 elseif errmsg then
369 ui.print(errmsg)
371 return pos
374 -- flush any code stuck in the fifo, so starting a REPL after the file has been closed won't read old stuff in.
375 events.connect(events.QUIT, function()
376 if repl_fifo then
377 repl_fifo:close()
378 repl_fifo = io.open(M.repl_fifo, "w+")
379 repl_fifo:close()
380 repl_fifo = nil
382 end)
384 H.I.cut = {H.O.change, do_copy = true}
385 H.I.copy = {H.O.yank, do_copy = true --[[XXX: just so the clipboard is emptied first]]}
386 H.I.backward_delete = {H.O.change, backward_char}
387 H.I.forward_delete = {H.O.change, forward_char}
388 H.I.backward_kill_word = {H.O.change, H.M.backward_word, do_copy = true}
389 H.I.forward_kill_word = {H.O.change, H.M.forward_word, do_copy = true}
390 H.I.backward_kill_sexp = {H.O.change, H.M.prev_start, do_copy = true}
391 H.I.backward_kill_sexp1 = {H.O.change, H.M.prev_start}
392 H.I.kill_sexp = {H.O.change, H.M.next_finish, do_copy = true}
393 H.I.kill_sexp1 = {H.O.change, H.M.next_finish}
394 H.I.kill_sentence = {H.O.change, H.M.end_of_sentence, do_copy = true}
395 H.I.backward_kill_sentence = {H.O.change, H.M.beginning_of_sentence, do_copy = true}
396 H.I.backward_kill_line = {H.O.change, line_begin, do_copy = true}
397 H.I.backward_kill_line1 = {H.O.change, line_begin}
398 H.I.kill = {H.O.change, line_end, do_copy = true}
399 H.I.kill1 = {H.O.change, line_end}
400 H.I.splice_sexp_killing_backward = {H.O.delete, H.M.backward_up, do_copy = true}
401 H.I.splice_sexp_killing_backward1= {H.O.delete, H.M.backward_up}
402 H.I.splice_sexp_killing_forward = {H.O.delete, H.M.forward_up, do_copy = true}
403 H.I.splice_sexp_killing_forward1 = {H.O.delete, H.M.forward_up}
404 H.I.raise_sexp = {H.O.raise_sexp}
405 H.I.splice_sexp = {H.O.splice_sexp}
406 H.I.forward_slurp = {H.O.forward_slurp}
407 H.I.forward_barf = {H.O.forward_barf}
408 H.I.backward_slurp = {H.O.backward_slurp}
409 H.I.backward_barf = {H.O.backward_barf}
410 H.I.convolute_sexp = {H.O.convolute_sexp}
411 H.I.wrap_round = {H.O.wrap_round, H.M.next_finish}
412 H.I.wrap_square = {H.O.wrap_square, H.M.next_finish}
413 H.I.wrap_curly = {H.O.wrap_curly, H.M.next_finish}
414 H.I.meta_doublequote = {H.O.meta_doublequote, H.M.next_finish}
415 H.I.wrap_comment = {H.O.wrap_comment, H.M.next_finish}
416 H.I.enclose_round = {H.O.wrap_round, H.T.expand_region}
417 H.I.enclose_square = {H.O.wrap_square, H.T.expand_region}
418 H.I.enclose_curly = {H.O.wrap_curly, H.T.expand_region}
419 H.I.enclose_doublequote = {H.O.meta_doublequote, H.T.expand_region}
420 H.I.block_comment = {H.O.toggle_comment}
421 H.I.close_and_newline = {H.O.close_and_newline}
422 H.I.open_next_line = {H.O.open_next_line}
423 H.I.split_sexp = {H.O.split_sexp}
424 H.I.join_sexps = {H.O.join_sexps}
425 H.I.transpose_sexps = {H.O.transpose_sexps}
426 H.I.transpose_words = {H.O.transpose_words}
427 H.I.transpose_chars = {H.O.transpose_chars}
428 H.I.reindent_defun = {H.O.format, H.T.mark_defun}
429 H.I.eval_defun = {H.O.eval_defun, H.T.paragraph}
430 H.I.eval_last_sexp = {H.O.eval_defun, H.M.backward}
432 local function seek(selection)
433 return function(offset)
434 buffer.selection_n_caret[selection] = offset + 1
435 buffer.selection_n_anchor[selection] = offset + 1
439 function new.M(func)
440 return function()
441 for i = 1, buffer.selections do
442 local pos = buffer.selection_n_caret[i] - 1
443 seek(i)(func(range_by_pos(pos), pos) or pos)
445 buffer:scroll_range(buffer.selection_n_caret[1], buffer.selection_n_caret[buffer.selections])
446 if func == H.M.prev_section or func == H.M.next_section then
447 local align = buffer:line_from_position(buffer.current_pos) - buffer.first_visible_line
448 buffer.line_scroll(0, align)
450 return true
454 function new.T(textobject)
455 return function()
456 for i = buffer.selections, 1, -1 do
457 local range = {buffer.selection_n_anchor[i] - 1, buffer.selection_n_caret[i] - 1}
458 local pos = range[2]
459 table.sort(range)
460 range.start, range.finish = range[1], range[2]
461 range[1], range[2] = nil, nil
462 local start, finish = textobject(range, pos)
463 if start and finish then
464 if buffer.selections == 1 then
465 buffer:set_selection(finish + 1, start + 1)
466 else
467 buffer:add_selection(finish + 1, start + 1)
474 function new.I(params)
475 local operator, motion_or_textobject = unpack(params)
476 return function()
477 buffer:begin_undo_action()
478 if params.do_copy then do_copy = true end
479 local clips = {}
480 for i = buffer.selections, 1, -1 do
481 local pos = buffer.selection_n_caret[i] - 1
482 local anchor = buffer.selection_n_anchor[i] - 1
483 local range = {start = math.min(anchor, pos), finish = math.max(anchor, pos)}
484 if motion_or_textobject and (range.finish == range.start) then
485 local start, finish = motion_or_textobject(range, pos)
486 if start and not finish then
487 range = {start = math.min(pos, start), finish = math.max(pos, start)}
488 elseif start and finish then
489 range = {start = start, finish = finish}
492 local newpos, fragments = operator(range, pos)
493 if params.do_copy and fragments and #fragments > 0 then
494 for _, v in ipairs(fragments) do
495 table.insert(clips, v)
498 seek(i)(newpos or pos)
500 do_copy = false
501 if params.do_copy and #clips > 0 then
502 local clipboard = table.concat(clips)
503 buffer:copy_text(clipboard)
505 buffer:end_undo_action()
506 return true
510 local function mnemonics_off()
511 if CURSES then return end
512 if not initialized then return end -- wait for the storm of events to pass
513 local filetype = buffer.lexer_language
514 if not supported[filetype] then return end
515 orig.menus = orig.menus or {}
516 for _, menu in ipairs(textadept.menu.menubar) do
517 local mnemonic = menu.title:match"&%a"
518 mnemonic = mnemonic and mnemonic:sub(2, 2):lower()
519 if mnemonic and keys[filetype] and keys[filetype][ALT..mnemonic] then
520 local new_title = menu.title:gsub("&", "", 1)
521 orig.menus[new_title] = menu.title
522 menu.title = new_title
527 local function mnemonics_on()
528 if CURSES then return end
529 if not initialized then return end -- wait for the storm of events to pass
530 local filetype = buffer.lexer_language
531 if not supported[filetype] then return end
532 for _, menu in ipairs(textadept.menu.menubar) do
533 if orig.menus[menu.title] then
534 menu.title = orig.menus[menu.title]
539 local extended_identifier_chars = "!$%&*+-./:<=>?@^_~"
541 local function settings_backup()
542 if not initialized then return end -- wait for the storm of events to pass
543 local filetype = buffer.lexer_language
544 if not supported[filetype] then return end
545 if not buffer.word_chars:find(extended_identifier_chars, 1, true) then
546 buffer.word_chars = extended_identifier_chars .. buffer.word_chars
548 orig.auto_indent = textadept.editing.auto_indent
549 orig.strip_trailing_spaces = textadept.editing.strip_trailing_spaces
550 orig.auto_pairs = textadept.editing.auto_pairs
551 orig.typeover_auto_paired = textadept.editing.typeover_auto_paired
553 textadept.editing.auto_indent = false
554 textadept.editing.strip_trailing_spaces = false
555 textadept.editing.auto_pairs = nil
556 textadept.editing.typeover_auto_paired = nil
559 local function settings_restore()
560 if not initialized then return end -- wait for the storm of events to pass
561 local filetype = buffer.lexer_language
562 if not supported[filetype] then return end
563 textadept.editing.auto_indent = orig.auto_indent
564 textadept.editing.strip_trailing_spaces = orig.strip_trailing_spaces
565 textadept.editing.auto_pairs = orig.auto_pairs
566 textadept.editing.typeover_auto_paired = orig.typeover_auto_paired
569 local function setup() mnemonics_off() settings_backup() end
570 local function teardown() mnemonics_on() settings_restore() end
572 events.connect(events.BUFFER_BEFORE_SWITCH, function() teardown() end)
573 events.connect(events.BUFFER_AFTER_SWITCH, function() setup() end)
574 events.connect(events.VIEW_BEFORE_SWITCH, function() teardown() end)
575 events.connect(events.VIEW_AFTER_SWITCH, function() setup() end)
577 events.connect(events.INITIALIZED, function()
578 for kind, register_new in pairs(new) do
579 for name, handler in pairs(H[kind]) do
580 B[kind][name] = register_new(handler)
583 local suffix = CURSES and "-curses" or ""
584 local keyconfig = {require(cwd..".keytheme.CUA"..suffix)(B, H),
585 M.emacs and require(cwd..".keytheme.emacs"..suffix)(B, H) or nil}
586 local inverted = {}
587 for _, config in ipairs(keyconfig) do
588 for func, key in pairs(config) do
589 if type(key) == "table" then
590 for _, k in ipairs(key) do
591 inverted[k] = func
593 for k, j in pairs(key) do
594 if type(k) ~= "number" and (not inverted[k] or type(inverted[k]) ~= "table") then
595 inverted[k] = {}
597 if type(inverted[k]) == "table" then
598 inverted[k][j] = func
601 else
602 inverted[key] = func
606 for dialect in pairs(supported) do
607 if not keys[dialect] then keys[dialect] = {} end
608 for key, binding in pairs(inverted) do
609 keys[dialect][key] = binding
612 initialized = true
613 setup()
614 end)
616 events.connect(events.FILE_OPENED, function()
617 local filetype = buffer.lexer_language
618 if not supported[filetype] then return end
619 local function write(pos, txt)
620 buffer:set_target_range(pos + 1, pos + 1)
621 buffer:replace_target(txt)
623 local function delete(pos, len)
624 return buffer:delete_range(pos + 1, len)
626 local function read(base, len)
627 if base == buffer.length - 1 then return end
628 base = (base or 0) + 1
629 len = len or buffer.length
630 local more = base + len <= buffer.length
631 return buffer:text_range(base, base + len), more
633 -- TODO: this can be implemented in the core modules, without using editor-specific APIs:
634 local function eol_at(pos)
635 return buffer.line_end_position[buffer:line_from_position(pos + 1)] - 1
637 -- TODO: this can be implemented in the core modules, without using editor-specific APIs:
638 local function bol_at(pos)
639 return buffer:position_from_line(buffer:line_from_position(pos + 1)) - 1
641 env[buffer] = init(filetype, read, write, delete, eol_at, bol_at)
642 local consumed_char = {"\n", " "}
643 for ch in pairs(env[buffer].parser.opposite) do
644 if #ch == 1 then
645 table.insert(consumed_char, ch)
648 env[buffer].consumed_char = "[" .. table.concat(consumed_char, "%") .. "]"
649 setup()
650 end)
652 events.connect(events.BUFFER_DELETED, function()
653 for b in pairs(env) do
654 if not _BUFFERS[b] then
655 env[b] = nil
658 end)
660 events.connect(events.MODIFIED, function(pos, mtype)
661 local filetype = buffer.lexer_language
662 if not supported[filetype] then return end
663 local text_inserted, text_deleted = 0x01, 0x02
664 if 0 == (mtype & (text_inserted | text_deleted)) then return end
665 if env[buffer].parser.tree.is_parsed(pos - 1) then
666 env[buffer].parser.tree.rewind(pos - 1)
668 end)
670 events.connect(events.KEYPRESS, function(key)
671 if ui.command_entry.active then return end
672 local filetype = buffer.lexer_language
673 if not supported[filetype] then return end
674 if key:find(CTRL) or key:find(ALT) or key:find(SHIFT) then return end
675 local ret
676 local winput = env[buffer].input
677 local consumed_char = key:find(env[buffer].consumed_char)
678 if consumed_char then buffer:begin_undo_action() end
679 for i = buffer.selections, 1, -1 do
680 local anchor, caret = buffer.selection_n_anchor[i] - 1, buffer.selection_n_caret[i] - 1
681 local range = {start = math.min(anchor, caret), finish = math.max(anchor, caret)}
682 if key == "\n" then
683 local sexp, parent = env[buffer].walker.sexp_at(range, true)
684 if parent.is_root and (not sexp or (not sexp.is_comment and sexp.finish + 1 == range.start)) then
685 local same_line = not not sexp
686 if not sexp then
687 -- FIXME: this assumes only one selection:
688 local line, pos = buffer.get_cur_line(), buffer.current_pos - 1
689 local prev_finish = env[buffer].walker.finish_before(range)
690 if prev_finish then
691 seek(i)(prev_finish)
692 same_line = line == buffer.get_cur_line()
693 seek(i)(pos)
696 if sexp or same_line then
697 eval_repl_line(range, buffer.selection_n_caret[i] - 1)
701 ret = winput.insert(range, seek(i), key, M.auto_square_brackets)
703 if consumed_char then buffer:end_undo_action() end
704 return ret
705 end)
707 return M