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