-
Notifications
You must be signed in to change notification settings - Fork 2
/
buttons.el
512 lines (446 loc) · 22.8 KB
/
buttons.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
;;; buttons.el --- Define and visualize hierarchies of keymaps -*- lexical-binding: t; -*-
;; Copyright (C) 2018, Ernesto Alfonso, all rights reserved.
;; Author: Ernesto Alfonso
;; Maintainer: (concat "erjoalgo" "@" "gmail" ".com")
;; Keywords: lisp extensions convenience tools
;; Created: 16 Sep 2018
;; Package-Requires: ((emacs "24.1") (cl-lib "0.3"))
;; URL: http://github.com/erjoalgo/emacs-buttons
;; Version: 0.0.4
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License
;; as published by the Free Software Foundation; either version 3
;; of the License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; A library and template language to define and visualize keymap hierarchies.
;;; Code:
(require 'cl-lib)
(defvar buttons-make-key-mapper #'identity
"A function used to map key definitions within a ‘buttons-make’ form.
It should be bound at compile-time via ‘let-when'")
(defvar buttons-make-self-help-binding (kbd "s-?")
"Key where to install the help visualizer in a buttons-make-defined keymap.")
(defmacro buttons-make (&rest bindings)
"Create a sparse keymap.
BINDINGS... is a list of (KEY TARGET) pairs, where KEY
should be suitable for use as the KEY argument in DEFINE-KEY,
for example \"<s-f1>\".
TARGET may be any value that could be passed to the DEF
argument of DEFINE-KEY, including a command and a keymap,
including an anonymous keymap created with BUTTONS-MAKE.
BUTTONS-MAKE-KEY-MAPPER, if non-nil, specifies
a function to apply to the KEY of each binding
before it is passed to DEFINE-KEY.
As an example, it may be used to add a modifier to
its input key to make the BINDINGS list more concise."
(let ((kmap-sym (cl-gentemp "kmap-")))
`(let ((,kmap-sym (make-sparse-keymap)))
(when buttons-make-self-help-binding
(define-key ,kmap-sym buttons-make-self-help-binding
((lambda (kmap-sym)
(defalias (make-symbol "keymap-help")
`(lambda () (interactive)
(buttons-display
(unless current-prefix-arg ',kmap-sym)))
"Keymap self-help."))
,kmap-sym)))
,@(cl-loop
for (key-spec value . rest) in bindings
when rest do (error "Malformed key definition: %s %s" key-spec value)
as key = (funcall buttons-make-key-mapper key-spec)
collect `(define-key ,kmap-sym ,key ,value))
,kmap-sym)))
(defun buttons-modifier-add-super (key-spec)
"Add the supper modifier to KEY-SPEC, if it is a string.
If KEY-SPEC is a string, then prefix it with the super modifier,
otherwise leave it intact.
Suitable as the value of BUTTONS-MAKE-KEY-MAPPER in ‘buttons-make'"
(cl-typecase key-spec
(string (kbd (format
(if (= (length key-spec) 1)
"s-%s"
"<s-%s>")
key-spec)))
(t key-spec)))
(defmacro defbuttons (kmap-sym ancestor-kmap target-keymap-syms keymap)
"Define a keymap KMAP-SYM.
ANCESTOR-KMAP, if non-nil,is merged recursively onto
KMAP-SYM via BUTTONS-DEFINE-KEYMAP-ONTO-KEYMAP.
TARGET-KEYMAP-SYMS is a list of keymap symbols, bound or unbound,
onto which to define KMAP-SYM via BUTTONS-AFTER-SYMBOL-LOADED-FUNCTION-ALIST.
KEYMAP is the keymap, for example, one defined via BUTTONS-MAKE."
(declare (indent 3))
(let* ((sym-name (symbol-name kmap-sym)))
`(progn
(defvar ,kmap-sym nil ,(format "%s buttons map" sym-name))
(setf ,kmap-sym ,keymap)
,@(when ancestor-kmap
`((buttons-define-keymap-onto-keymap ,ancestor-kmap ,kmap-sym ',kmap-sym t)))
,@(cl-loop for orig in (if (and target-keymap-syms
(atom target-keymap-syms))
(list target-keymap-syms)
target-keymap-syms)
as form = `(buttons-define-keymap-onto-keymap ,kmap-sym ,orig ',orig)
append
(if (boundp orig)
`(,form)
`((push (cons ',orig (lambda () ,form))
buttons-after-symbol-loaded-function-alist)))))))
(defun keymap-symbol (keymaps)
"Return the symbol to which any keymap in KEYMAPS is bound."
(let (syms)
(mapatoms (lambda (sym)
(and (not (eq sym 'keymap))
(boundp sym)
(cl-find (symbol-value sym) keymaps)
(push sym syms))))
syms))
(defun buttons-define-keymap-onto-keymap (from-map to-map &optional from-sym no-overwrite-p)
"Define bindings FROM-MAP onto TO-MAP, recursively.
If a binding A in FROM-MAP doesn't exist on TO-MAP, define A onto TO-MAP.
Otherwise, if a binding is a prefix key on both maps, merge recursively.
Otherwise FROM-MAP's binding overwrites TO-MAP's binding
only when NO-OVERWRITE-P is non-nil.
The optional argument FROM-SYM is used for visualization."
(cl-labels
((merge
(from-map to-map &optional path)
(map-keymap
(lambda (key cmd)
(let* ((keyvec (vector key))
(existing (lookup-key to-map keyvec)))
(cond
((and (keymapp cmd) (keymapp existing))
(merge cmd existing (cons (key-description keyvec) path)))
((or (not no-overwrite-p) (not existing))
(when (and existing (keymapp existing))
(warn
(concat "%s overwrites nested keymap with plain command "
"on %s %s in map %s: %s overwrites %s")
(or (symbol-name from-sym) "child")
(key-description keyvec)
(or (reverse path) "")
(keymap-symbol (list to-map))
cmd
existing))
(define-key to-map keyvec cmd)))))
from-map)
to-map))
(merge from-map to-map)))
(defvar buttons-after-symbol-loaded-function-alist nil
"An alist where each element has the form (SYMBOL . FUNCTION).
FUNCTION takes no arguments and is evaluated after SYMBOL has been bound.
If SYMBOL is currently bound, FUNCTION is called immediately.")
(defun buttons-after-symbol-loaded (_file-loaded)
"Function invoked after new symbols may have been defined in FILE-LOADED.
Iterates over list of pending items in
‘buttons-after-symbol-loaded-function-alist',
evaluating and removing entries for symbols that have become bound."
(setf buttons-after-symbol-loaded-function-alist
(cl-loop for (sym . fun) in buttons-after-symbol-loaded-function-alist
if (boundp sym) do
(progn
(condition-case err (funcall fun)
('error
(warn "WARNING: unable to load action %s for symbol %s: %s"
sym fun err))))
else collect (cons sym fun))))
(add-hook 'after-load-functions #'buttons-after-symbol-loaded)
(defun buttons-read-keymap ()
"Interactively read a keymap symbol. Based on ‘help-fns+'."
(intern
(completing-read "Keymap: " obarray
(lambda (m) (and (boundp m)))
t
(when (symbol-at-point)
(symbol-name (symbol-at-point)))
'variable-name-history)))
(defun buttons-display (&optional keymap hide-command-names hide-command-use-count)
"Visualize a keymap KEYMAP in a help buffer.
Unlike the standard keymap bindings help, nested keymaps
are visualized recursively. This is suitable for visualizing
BUTTONS-MAKE-defined nested keymaps.
If HIDE-COMMAND-NAMES is non-nil, command names are not displayed.
If HIDE-COMMAND-USE-COUNT is non-nil, no attempt is made to display
recorded command use-counts.
When called with a nil keymap, or interactively with a prefix argument,
all currently active keymaps are displayed."
(interactive (unless current-prefix-arg
(list (buttons-read-keymap))))
(let ((min-sep 2) (max-command-name-length 30) (use-count-padding 6))
(cl-labels ((event-to-string (event)
(key-description (vector event)))
(print-key (event)
(princ (event-to-string event)))
(spaces (len) (make-string len 32))
(maybe-truncate (string max)
(if (>= max (length string))
string
(cl-assert (>= max 3))
(concat (cl-subseq string 0 (- max 3)) "...")))
(remove-newlines (string)
(replace-regexp-in-string "\n" "\\\\n" string))
(print-command (binding)
(unless hide-command-names
(if (and (commandp binding);;not a keymap
(symbolp binding);;not an anonymous lambda
binding)
(insert-text-button
(maybe-truncate (remove-newlines (prin1-to-string binding))
max-command-name-length)
:type 'help-function
'help-args (list binding)
'button '(t))
(princ (remove-newlines (prin1-to-string binding)))))
(unless hide-command-use-count
(let ((use-count-width
(and (symbolp binding)
(< 0 (or (get binding 'use-count) 0))
(length (princ
(format "<%d>" (get binding 'use-count)))))))
(princ (spaces (- use-count-padding (or use-count-width 0))))))
(when (and (commandp binding)
(documentation binding))
(princ (spaces min-sep))
(princ (remove-newlines (documentation binding)))))
(print-keymap (keymap level sep)
(map-keymap (lambda (event binding)
(princ (spaces (* level sep)))
(let ((event-desc (print-key event)))
(cl-assert (> sep (length event-desc)))
(princ (spaces (- sep (length event-desc)))))
(if (keymapp binding)
(progn
(princ "\n")
(print-keymap binding (1+ level) sep))
(print-command binding)
(princ "\n")))
keymap))
(find-keymap-descriptor (keymap)
(or
(cl-block nil
(mapatoms (lambda (sym)
(when (and
(not (eq sym 'keymap))
(boundp sym)
(eq (symbol-value sym) keymap))
(cl-return sym)))))
(cl-loop for (minor-mode-sym . kmap)
in minor-mode-map-alist
thereis
(when (equal kmap keymap)
(format "%s (minor-mode-map-alist)"
minor-mode-sym)))))
(max-event-length (keymap)
(let ((max 0))
(map-keymap
(lambda (event binding)
(setf max
(max max (length (event-to-string event))
(if (keymapp binding)
(max-event-length binding) 0))))
keymap)
max)))
(cl-destructuring-bind (name kmaps)
(cond
((null keymap) (list "(current-active-maps)" (current-active-maps)))
((symbolp keymap) (list (symbol-name keymap) (list (symbol-value keymap))))
(t (list (find-keymap-descriptor keymap) (list keymap))))
(let ((max-event-length (cl-loop for kmap in kmaps
maximize (max-event-length kmap)))
(buffer-name (format "*%s help*" (or name "keymap")))
(help-window-select t))
(with-help-window buffer-name
(with-current-buffer
buffer-name
(dolist (kmap kmaps)
(princ (or (find-keymap-descriptor kmap) "(anonymous keymap)"))
(add-text-properties (line-beginning-position)
(line-end-position)
'(face bold))
(princ ":\n")
(print-keymap kmap 0 (+ max-event-length min-sep))
(princ "\n\n\n"))
(toggle-truncate-lines t))))))))
(unless (lookup-key help-map "M")
(define-key help-map "M" #'buttons-display))
(defvar buttons-template-insert-directive-regexp
"{\\(.*?\\)}"
"Determines what `buttons-template-insert' interprets as a directive.
BUTTONS-TEMPLATE-INSERT-DIRECTIVE-REGEXP may be used to set the regexp
that defines directives to interpret. The first capture group is used
as the directive contents. Note that this variable should be bound
via ‘let-when-compile' instead of ‘let' to make this binding available
at macro-expansion time.")
(defmacro buttons-template-insert (&rest templates)
"Compile a string template into a progression of LISP commands.
The template may be split into several arguments TEMPLATES,
each of which is compiled. If an argument is not a string,
it is used as a raw LISP expression. Otherwise,
Any directive {DIRECTIVE} within curly braces is interpreted:
If DIRECTIVE is the empty string, the function
‘buttons-record-template-var'is invoked to allow the user to enter text.
If DIRECTIVE is a number K, the function ‘buttons-record-template-var'
is invoked to allow the user to enter text on the first occurrence
of the directive K in the template, and on subsequent occurrences
the recorded text is entered without prompt.
Otherwise, DIRECTIVE is interpreted as a LISP expression.
If the expression evaluates to a string, it is inserted.
Any text outside directives is inserted literally.
BUTTONS-TEMPLATE-INSERT-DIRECTIVE-REGEXP may be used to change the regexp
that defines directives to interpret. The first capture group is used
as the directive contents. Note that this variable should be bound
via ‘let-when-compile' instead of ‘let' to make this binding available
at macro-expansion time. Also note that a substring is not considered
a directive if it does not match the directive regexp within a single
string.
Example:
for ( int {0} = 0; {0} < {}; {0}++ ){(cbd)}
Expands into:
- insert 'for ( int '
- enter recursive edit and record entered text as a string labeled '0'
- insert ' = ; '
- insert the already-recorded string 0
- insert ' < '
- enter recursive edit, no recording is done
- enter '; '
- insert the already-recorded string 0
- insert '++ )
- expand into the form: (cbd), which should be a valid LISP expression"
(cl-loop for tmpl in templates
with forms = nil
with rec-sym-alist = nil
with directive-regexp = buttons-template-insert-directive-regexp
with insert-if-string =
(lambda (form)
(let ((expr-val-sym (gensym "expr-val-")))
`(let* ((,expr-val-sym ,form))
(when (stringp ,expr-val-sym)
(insert ,expr-val-sym)))))
do
(if (not (stringp tmpl))
(push (funcall insert-if-string tmpl) forms)
(cl-loop with start = 0
as rec-capture-start = (string-match directive-regexp tmpl start)
do (if rec-capture-start
(progn
(unless (= start rec-capture-start)
(push `(insert ,(cl-subseq tmpl start rec-capture-start)) forms))
(let ((group-no-str (match-string 1 tmpl))
(match-data (match-data)))
(cond
((zerop (length group-no-str))
(push `(buttons-record-template-var) forms))
((string-match "^[0-9]+$" group-no-str)
(let* ((group-no (string-to-number group-no-str))
(sym (cdr (assoc group-no rec-sym-alist))))
(if sym
(push `(insert ,sym) forms)
(setf sym (gensym (format "rec-capture-%d-" group-no)))
(push (cons group-no sym) rec-sym-alist)
(push `(setf ,sym (buttons-record-template-var)) forms))))
(t (push (funcall insert-if-string (read group-no-str))
forms)))
(set-match-data match-data)
(setf start (match-end 0))))
(progn (when (< start (length tmpl))
(push `(insert ,(cl-subseq tmpl start)) forms))
(setf start (length tmpl))))
while rec-capture-start))
finally (return `(let ,(mapcar 'cdr rec-sym-alist)
;; (doc ,tmpl)
,@(reverse forms)))))
(defcustom buttons-record-template-var-method
'recedit
"Specifies how ‘buttons-record-template-var' should prompt for template
variables."
:type 'symbol
:group 'emacs-buttons)
(defun buttons-record-template-var ()
"Insert and record some text from the user.
If the value of ‘buttons-record-template-var' is
- 'recedit: enter a recursive edit and record any text entered there
- 'prompt: use a minibuffer prompt."
(cl-case buttons-record-template-var-method
('recedit (let ((old-point (point)))
(recursive-edit)
(buffer-substring-no-properties old-point (point))))
('prompt (insert (read-string "enter template variable: ")))
((t) (error "Invalid value: %s" buttons-record-template-var-method))))
(defmacro buttons-defcmd (&rest body)
"Define an anonymous command with body BODY.
The number of times the command is invoked is recorded
as the USE-COUNT property of the function symbol.
This may be useful for analysis and for making
decisions about which bindings' key-sequence
lengths are worth shortening."
(cl-loop for form in body
with forms = nil
with doc = nil
with cmd-name = (cl-gentemp "autogen-cmd-")
with point-original-sym = (gensym "point-original-")
do (if (and (consp form)
(eq (car form) 'doc))
(push (cadr form) doc)
(push form forms))
finally
(return
`(progn
(put ',cmd-name 'use-count (or (get ',cmd-name 'use-count) 0))
(defun ,cmd-name ()
,(apply 'concat (reverse (mapcar 'prin1-to-string forms)))
(interactive)
(cl-incf (get ',cmd-name 'use-count))
(cl-block ,cmd-name
(let ((,point-original-sym (point)))
(catch 'buttons-abort
,@(reverse forms)
(cl-return-from ,cmd-name))
;; aborted. undoing...
(undo-boundary)
(delete-region ,point-original-sym (point)))))))))
(defun buttons-abort-cmd ()
"Throw the tag required to abort the current buttons-defined command."
(interactive)
(message "aborting buttons command...")
(throw 'buttons-abort nil))
(defun buttons-insert-c-style-code-block ()
"Insert a c-style code block with curly braces."
(interactive)
(insert " {")
(newline-and-indent)
(recursive-edit)
(newline)
(insert "}")
(indent-for-tab-command))
(defmacro buttons-transform-region (current-text-sym &rest body)
"Replace the current region text CURRENT-TEXT-SYM with the value BODY."
`(save-excursion
(let ((,current-text-sym (buffer-substring (region-beginning) (region-end))))
(delete-region (region-beginning) (region-end))
(insert (progn ,@body)))))
(defmacro buttons-macrolet (more-macrolet-defs &rest body)
"Make short aliases of useful button-related forms available within BODY.
Provides a compact DSL for defining buttons.
MORE-MACROLET-DEFS specifies additional user-defined cl-macrolet forms."
(declare (indent 1))
`(cl-macrolet
((but (&rest rest) `(buttons-make ,@rest))
(nli () `(newline-and-indent))
(ins (&rest text) `(buttons-template-insert ,@text))
(cmd (&rest rest) `(buttons-defcmd ,@rest))
(cbd () `(buttons-insert-c-style-code-block))
(rec () `(recursive-edit))
(idt () `(indent-for-tab-command))
(cmt (&rest rest) `(comint-send-input ,@rest))
(cmd-ins (&rest rest) `(cmd (ins ,@rest)))
,@more-macrolet-defs)
,@body))
(provide 'buttons)
;;; buttons.el ends here