link to fmt configuration as example
[howl-parkour.git] / init.moon
bloba00599501a95086ad300f73f3b23a951fa45f519
1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
4 -- would be nice if Howl had this prepared for plugins to use:
5 for dir in *howl.bundle.dirs
6   package.path = tostring(dir).."/?.lua;" .. tostring(dir).."/?/init.lua;" .. package.path
8 -- XXX: just in case it gets fixed:
9 cwd = ...
11 -- XXX: just in case Howl switches to Lua > 5.1
12 unpack = table.unpack or unpack
14 init, supported = unpack require (cwd or "howl-parkour")..".parkour"
16 filetype =
17   scm:  "scheme",  sch: "scheme",   ss: "scheme", sls: "scheme", sld: "scheme",
18   lisp: "lisp",    lsp: "lisp",     el: "lisp",   cl: "lisp",
19   clj:  "clojure", cljs: "clojure",
20   fnl:  "fennel"
22 H = M: {}, T: {}, O: {}  -- handler functions
23 C = howl.config
25 class Keymaps
26   tbl: {}
27   new: =>
28     @tbl = {bundle_load "keytheme.CUA", H}
29   install: =>
30     @emacs C.emacs
31     for keymap in *@tbl
32       howl.bindings.push keymap
33   remove: =>
34     for keymap in *@tbl
35       howl.bindings.remove keymap
36   emacs: (on) =>
37     if on
38       if #@tbl == 1 then table.insert @tbl, (bundle_load "keytheme.emacs", H)
39     elseif #@tbl > 1
40       table.remove @tbl
42 keymaps = Keymaps!
44 with C
45   .define name: "auto_square_brackets", type_of: "boolean", scope: "global", description: "Force square brackets where appropriate"
46   .define name: "repl_fifo",            type_of: "string",  scope: "global", description: "Named fifo to send sexps to (REPL)"
47   .define name: "emacs",                type_of: "boolean", scope: "global", description: "Enable emacs key bindings"
48   .define name: "lispwords",                                scope: "global", description: "Indent numbers for functions/macros"
50 env = {}
51 do_copy = false
53 howl.signal.connect "app-ready", ->
54   C.watch("emacs", -> keymaps\remove!  keymaps\install!)
56 C.lispwords = setmetatable {}, __index: (t, ftype) ->
57   if supported[ftype]
58     recfg = __newindex: (_, k, v) ->
59       howl.signal.connect "file-opened", (args) ->
60         {:file, :buffer} = args
61         dialect = filetype[file.extension]
62         if dialect == ftype
63           env[buffer].fmt.lispwords[k] = v
64     t[ftype] = setmetatable {}, recfg
65     t[ftype]
67 howl.signal.connect "file-opened", (args) ->
68   {:file, :buffer} = args
69   dialect = filetype[file.extension]
70   return if not supported[dialect]
71   write = (pos, txt) ->
72     buffer\insert txt, pos + 1
73   delete = (pos, len) ->
74     buffer\delete pos + 1, pos + len
75   read = (base, len) ->
76     return if base == buffer.size - 1
77     base = base or 0
78     len = len or buffer.size
79     more = base + len < buffer.size
80     (buffer\sub base + 1, base + len), more
81   eol_at = (pos) ->
82     (buffer.lines\at_pos pos + 1).end_pos - 1
83   bol_at = (pos) ->
84     (buffer.lines\at_pos pos + 1).start_pos - 1
85   env[buffer] = init dialect, read, write, delete, eol_at, bol_at
86   consumed_char = {"\n", " "}
87   for ch in pairs env[buffer].parser.opposite do (table.insert consumed_char, ch) if #ch == 1
88   env[buffer].consumed_char = "[" .. (table.concat consumed_char, "%") .. "]"
90 rewind = (args) ->
91   {:buffer, :at_pos} = args
92   return if not (buffer.file and supported[filetype[buffer.file.extension]] and
93                  env[buffer] and env[buffer].parser.tree.is_parsed at_pos - 1)
94   env[buffer].parser.tree.rewind at_pos - 1
96 howl.signal.connect "text-deleted", rewind
97 howl.signal.connect "text-changed", rewind
98 howl.signal.connect "text-inserted", rewind
100 seek = (cursor) ->
101   (offset) ->
102     cursor\move_to pos: offset + 1
104 howl.signal.connect "key-press", (args) ->
105   {:parameters, :event, :source} = args
106   editor = source == "editor" and parameters[1]
107   buffer = editor and editor.buffer
108   return if not (editor and buffer.file and
109                  supported[filetype[buffer.file.extension]] and
110                  event.character and
111                  not (event.control or event.meta or event.alt or
112                       event.key_name == "backspace" or
113                       event.key_name == "delete"))
114   input = env[buffer].input
115   pos = editor.cursor.pos - 1
116   range = start: pos, finish: pos
117   if not editor.selection.empty
118     start, finish = editor.selection\range!
119     range = start: start - 1, finish: finish - 1
120   char = event.character == "\r" and "\n" or event.character
121   if char == "\n"
122     sexp, parent = env[buffer].walker.sexp_at range, true
123     if parent.is_root and (not sexp or (not sexp.is_comment and sexp.finish + 1 == range.start))
124       same_line = not not sexp
125       if not sexp
126         line = buffer.lines\at_pos pos + 1
127         prev_finish = env[buffer].walker.finish_before range
128         if prev_finish
129           (seek editor.cursor) prev_finish
130           same_line = line == buffer.lines\at_pos editor.cursor.pos
131           (seek editor.cursor) pos
132       if sexp or same_line
133         rl = env[buffer].walker.repl_line_at range
134         rl.finish += 1
135         H.O.eval_defun env[buffer], rl, pos
136   M = buffer.config
137   consumed_char = char\find env[buffer].consumed_char
138   flag = {}
139   if consumed_char
140     buffer\as_one_undo ->
141       flag.set = input.insert range, (seek editor.cursor), char, M.auto_square_brackets
142   else
143     flag.set = input.insert range, (seek editor.cursor), char, M.auto_square_brackets
144   howl.signal.abort if flag.set
146 howl.signal.connect "after-buffer-switch", (args) ->
147   {:current_buffer, :editor} = args
148   keymaps\remove!
149   return if not current_buffer.file
150   dialect = filetype[current_buffer.file.extension]
151   return if not supported[dialect]
152   keymaps\install!
153   editor.mode_at_cursor.auto_pairs = {}
154   editor.motion = (func) =>
155     pos = @cursor.pos - 1
156     range = start: pos, finish: pos
157     newpos = func env[current_buffer], range
158     if newpos and newpos ~= pos then (seek @cursor) newpos
159   editor.extend = (func) =>
160     pos = @cursor.pos - 1
161     range = start: pos, finish: pos
162     local backward
163     if not @selection.empty
164       start, finish = @selection\range!
165       range = start: start - 1, finish: finish - 1
166       backward = @selection.cursor < @selection.anchor
167     newpos, textobject_finish = func env[current_buffer], range, pos
168     local left, right
169     if newpos and not textobject_finish
170       left, right = (math.min range.start, range.finish, newpos) + 1, (math.max range.start, range.finish, newpos) + 1
171     elseif newpos and textobject_finish
172       left, right = (math.min newpos, textobject_finish) + 1, (math.max newpos, textobject_finish) + 2
173     if left and right
174       @selection\set (backward and right or left), (backward and left or right)
175   editor.operator = (func, motion, _copy) =>
176     pos = @cursor.pos - 1
177     range = start: pos, finish: pos
178     if not @selection.empty
179       start, finish = @selection\range!
180       range = start: start - 1, finish: finish - 1
181     elseif motion
182       start, finish = motion env[current_buffer], range
183       if start and finish
184         range = start: start, finish: finish + 1
185       elseif start
186         range = start: (math.min pos, start), finish: (math.max pos, start)
187     t = {}
188     current_buffer\as_one_undo ->
189       if _copy
190         do_copy = true
191       t.newpos, fragments = func env[current_buffer], range, pos
192       do_copy = false
193       if _copy and fragments and #fragments > 0
194         clip = table.concat fragments
195         howl.clipboard.push clip
196     if t.newpos then (seek @cursor) t.newpos
198 -- motions:
200 is_comment = (t) -> t.is_comment
202 H.M.word_right_end  = (range) =>
203   escaped = @walker.escaped_at range
204   if escaped
205     @walker.finish_word_after escaped, range
206   else
207     @walker.finish_after range, is_comment
209 H.M.word_left       = (range) =>
210   escaped = @walker.escaped_at range
211   if escaped
212     @walker.start_word_before escaped, range
213   else
214     @walker.start_before range, is_comment
216 H.M.forward_down    = (range) => @walker.start_down_after range, is_comment
217 H.M.backward_up     = (range) => @walker.start_up_before range
218 H.M.forward_up      = (range) => @walker.finish_up_after range
219 H.M.backward_down   = (range) => @walker.finish_down_before range, is_comment
220 H.M.para_up         = (range) =>
221   sexp = @walker.paragraph_at range
222   sexp and sexp.start
223 H.M.para_down       = (range) =>
224   sexp = @walker.paragraph_at range
225   sexp and sexp.finish + 1
226 H.M.right           = (range) => range.finish + 1
227 H.M.left            = (range) => range.start - (range.start > 0 and 1 or 0)
228 H.M.line_end        = (range) => (@walker.next_finish_wrapped range)
229 H.M.home            = (range) => @walker.prev_start_wrapped range
230 H.M.sentence_end    = (range) => @walker.anylist_finish range
231 H.M.sentence_start  = (range) => @walker.anylist_start range
233 H.M.next_section    = (range) =>
234   newpos = @walker.find_after range, (t) -> t.section and t.start > range.start
235   newpos and newpos.start or howl.app.editor.buffer.size
237 H.M.prev_section    = (range) =>
238   newpos = @walker.find_before range, (t) -> t.section
239   newpos and newpos.start or 0
241 H.M.mark_sexp       = (range, pos) =>
242   if pos < range.finish
243     H.M.word_left self, range
244   else
245     H.M.word_right_end self, range
247 H.M.backward        = (range) => @walker.start_before range, is_comment
248 H.M.forward         = (range) => @walker.finish_after range, is_comment
249 H.M.backward_word   = (range) => (@walker.start_float_before range, true)
250 H.M.forward_word    = (range) => (@walker.finish_float_after range, true)
252 -- textobjects:
254 H.T.paragraph = (range) =>
255   sexps = @walker.repl_line_at range
256   if sexps then sexps.start, sexps.finish
258 H.T.mark_defun = (range) =>
259   sexp = @walker.paragraph_at range
260   if sexp and not sexp.is_line_comment then sexp.start, sexp.finish
262 H.T.expand_region = (range) =>
263   start, finish = @walker.wider_than range
264   start, finish and finish - 1
266 H.T.line = (range) =>
267   l = howl.app.editor.buffer.lines\at_pos range.start + 1
268   l.start_pos - 1, l.end_pos - 1
270 -- operators:
272 make_yank = (handler, fragments) ->
273   (range) ->
274     txt = howl.app.editor.buffer\sub range.start + 1, range.finish
275     if txt
276       table.insert fragments, 1, txt
277     handler and handler range
279 _delete = (range) ->
280   howl.app.editor.buffer\delete range.start + 1, range.finish
282 H.O.delete = (range, pos) =>
283   fragments = {}
284   delete_maybe_yank = do_copy and make_yank(_delete, fragments) or _delete
285   splicing = true
286   newpos = @edit.delete_splicing range, pos, splicing, delete_maybe_yank
287   newpos, fragments
289 H.O.change = (range, pos) =>
290   fragments = {}
291   delete_maybe_yank = do_copy and make_yank(_delete, fragments) or _delete
292   newpos = @edit.delete_nonsplicing range, pos, delete_maybe_yank
293   newpos, fragments
295 H.O.yank = (range, pos) =>
296   fragments = {}
297   yank = make_yank nil, fragments
298   action = kill:false, wrap: true, splice: false, func: yank
299   @edit.pick_out range, pos, action
300   range.start, fragments
302 H.O.paste_reindent = =>
303   editor = howl.app.editor
304   buffer = editor.buffer
305   if howl.clipboard.current
306     buffer\as_one_undo ->
307       editor\paste!
308       pos = editor.cursor.pos - 1
309       cursor = start: pos, finish: pos
310       _, parent = @walker.sexp_at cursor, true
311       @edit.refmt_at parent, cursor
313 H.O.format = (range, pos) =>
314   @edit.refmt_at range, {start: pos, finish: pos}
316 local repl_fifo
318 H.O.eval_defun = (range, pos) =>
319   M = howl.app.editor.buffer.config
320   if not M.repl_fifo or range.finish == range.start then return pos
321   if pos > range.finish then return pos
322   unbalanced = @parser.tree.unbalanced_delimiters range
323   if unbalanced and #unbalanced > 0 then return pos
324   local errmsg
325   if not repl_fifo
326     repl_fifo, errmsg = io.open M.repl_fifo, "a+"
327   if repl_fifo
328     repl_fifo\write (howl.app.editor.buffer\sub range.start + 1, range.finish), "\n"
329     repl_fifo\flush!
330   elseif errmsg
331     log.warn errmsg
332   pos
334 -- flush any code stuck in the fifo, so starting a REPL after the file has been closed will not read old stuff in.
335 howl.signal.connect "buffer-closed", (args) ->
336   {:buffer} = args
337   dialect = buffer.file and filetype[buffer.file.extension]
338   return if not supported[dialect]
339   env[buffer] = nil
340   if repl_fifo
341     for _ in pairs env  -- instead of "quit" event
342       return
343     M = buffer.config
344     repl_fifo\close!
345     repl_fifo = io.open M.repl_fifo, "w+"
346     repl_fifo\close!
347     repl_fifo = nil
349 H.O.block_comment = (range, pos) =>
350   sexp = @walker.sexp_at range, true
351   if sexp and not sexp.is_comment
352     @edit.wrap_comment {start: sexp.start, finish: sexp.finish + 1}, pos
353   elseif sexp
354     @edit.splice_anylist range, pos
356 H.O.wrap_comment = (range, pos) =>
357   @edit.wrap_comment {start: pos, finish: range.finish + 1}, pos
359 H.O.close_and_newline = (range) => @edit.close_and_newline range
360 H.O.open_next_line    = (range) => @edit.close_and_newline range, "\n"
362 H.O.raise_sexp       = (range, pos) => @edit.raise_sexp range, pos
363 H.O.splice_sexp      = (range, pos) => @edit.splice_anylist range, pos
364 H.O.split_sexp       = (range)      => @edit.split_anylist range
365 H.O.join_sexps       = (range)      => @edit.join_anylists range
366 H.O.convolute_sexp   = (range)      => @edit.convolute_lists range, pos
367 H.O.transpose_sexps  = (range)      => @edit.transpose_sexps range
368 H.O.transpose_words  = (range)      => @edit.transpose_words range
369 H.O.wrap_round       = (range, pos) => @edit.wrap_round range, pos, C.auto_square_brackets
370 H.O.wrap_square      = (range, pos) => @edit.wrap_square range, pos, C.auto_square_brackets
371 H.O.wrap_curly       = (range, pos) => @edit.wrap_curly range, pos, C.auto_square_brackets
372 H.O.meta_doublequote = (range, pos) => @edit.meta_doublequote range, pos, C.auto_square_brackets
373 H.O.forward_barf     = (range)      => @edit.barf_sexp range, true
374 H.O.backward_barf    = (range)      => @edit.barf_sexp range
375 H.O.forward_slurp    = (range)      => @edit.slurp_sexp range, true
376 H.O.backward_slurp   = (range)      => @edit.slurp_sexp range
378 howl.signal.connect "editor-focused", (args) ->
379   {:editor} = args
380   return if not editor.buffer.file
381   dialect = filetype[editor.buffer.file.extension]
382   keymaps\remove!
383   if supported[dialect]
384     keymaps\install!
386 unload = ->
387   env = {}
388   keymaps\remove!
391   info:
392     author: "Georgi Kirilov"
393     description: "Structured editing of S-expressions"
394     license: "TBD"
395   :unload