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:
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"
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",
22 H = M: {}, T: {}, O: {} -- handler functions
28 @tbl = {bundle_load "keytheme.CUA", H}
32 howl.bindings.push keymap
35 howl.bindings.remove keymap
38 if #@tbl == 1 then table.insert @tbl, (bundle_load "keytheme.emacs", H)
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"
53 howl.signal.connect "app-ready", ->
54 C.watch("emacs", -> keymaps\remove! keymaps\install!)
56 C.lispwords = setmetatable {}, __index: (t, ftype) ->
58 recfg = __newindex: (_, k, v) ->
59 howl.signal.connect "file-opened", (args) ->
60 {:file, :buffer} = args
61 dialect = filetype[file.extension]
63 env[buffer].fmt.lispwords[k] = v
64 t[ftype] = setmetatable {}, recfg
67 howl.signal.connect "file-opened", (args) ->
68 {:file, :buffer} = args
69 dialect = filetype[file.extension]
70 return if not supported[dialect]
72 buffer\insert txt, pos + 1
73 delete = (pos, len) ->
74 buffer\delete pos + 1, pos + len
76 return if base == buffer.size - 1
78 len = len or buffer.size
79 more = base + len < buffer.size
80 (buffer\sub base + 1, base + len), more
82 (buffer.lines\at_pos pos + 1).end_pos - 1
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, "%") .. "]"
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
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
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
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
126 line = buffer.lines\at_pos pos + 1
127 prev_finish = env[buffer].walker.finish_before range
129 (seek editor.cursor) prev_finish
130 same_line = line == buffer.lines\at_pos editor.cursor.pos
131 (seek editor.cursor) pos
133 rl = env[buffer].walker.repl_line_at range
135 H.O.eval_defun env[buffer], rl, pos
137 consumed_char = char\find env[buffer].consumed_char
140 buffer\as_one_undo ->
141 flag.set = input.insert range, (seek editor.cursor), char, M.auto_square_brackets
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
149 return if not current_buffer.file
150 dialect = filetype[current_buffer.file.extension]
151 return if not supported[dialect]
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
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
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
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
182 start, finish = motion env[current_buffer], range
184 range = start: start, finish: finish + 1
186 range = start: (math.min pos, start), finish: (math.max pos, start)
188 current_buffer\as_one_undo ->
191 t.newpos, fragments = func env[current_buffer], range, pos
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
200 is_comment = (t) -> t.is_comment
202 H.M.word_right_end = (range) =>
203 escaped = @walker.escaped_at range
205 @walker.finish_word_after escaped, range
207 @walker.finish_after range, is_comment
209 H.M.word_left = (range) =>
210 escaped = @walker.escaped_at range
212 @walker.start_word_before escaped, range
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
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
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)
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
272 make_yank = (handler, fragments) ->
274 txt = howl.app.editor.buffer\sub range.start + 1, range.finish
276 table.insert fragments, 1, txt
277 handler and handler range
280 howl.app.editor.buffer\delete range.start + 1, range.finish
282 H.O.delete = (range, pos) =>
284 delete_maybe_yank = do_copy and make_yank(_delete, fragments) or _delete
286 newpos = @edit.delete_splicing range, pos, splicing, delete_maybe_yank
289 H.O.change = (range, pos) =>
291 delete_maybe_yank = do_copy and make_yank(_delete, fragments) or _delete
292 newpos = @edit.delete_nonsplicing range, pos, delete_maybe_yank
295 H.O.yank = (range, pos) =>
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 ->
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}
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
326 repl_fifo, errmsg = io.open M.repl_fifo, "a+"
328 repl_fifo\write (howl.app.editor.buffer\sub range.start + 1, range.finish), "\n"
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) ->
337 dialect = buffer.file and filetype[buffer.file.extension]
338 return if not supported[dialect]
341 for _ in pairs env -- instead of "quit" event
345 repl_fifo = io.open M.repl_fifo, "w+"
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
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) ->
380 return if not editor.buffer.file
381 dialect = filetype[editor.buffer.file.extension]
383 if supported[dialect]
392 author: "Georgi Kirilov"
393 description: "Structured editing of S-expressions"