changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/sk.el

changeset 405: 1816f9c53453
parent: d876b572b5b9
child: 71baf0d4768d
author: Richard Westhaver <ellis@rwest.io>
date: Tue, 04 Jun 2024 18:53:40 -0400
permissions: -rw-r--r--
description: work on sk.el
1 ;;; sk.el --- skel Emacs Mode -*- lexical-binding: t; -*-
2 
3 ;; skel-mode, skel-minor-mode,skt-minor-mode, sk-classes
4 
5 ;; Copyright (C) 2023 The Compiler Company
6 
7 ;; Author: ellis <ellis@rwest.io>
8 ;; Keywords: languages, lisp
9 
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14 
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
19 
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
22 
23 ;;; Commentary:
24 
25 ;;; Code:
26 
27 (eval-and-compile (require 'eieio)
28  (require 'cl-lib)
29  (require 'sxp (expand-file-name "sxp.el" (join-paths user-emacs-directory "lib/")))
30  (require 'skeleton)
31  (require 'tempo)
32  (require 'autoinsert)
33  (defvar skel-debug nil)
34  (when skel-debug (require 'ede)))
35 
36 (defvar skel-version "0.1.0")
37 
38 (defgroup skel nil
39  "skel customization group."
40  :group 'local)
41 
42 (defcustom skel-minor-mode-map-prefix "C-c C-."
43  "Prefix for `skel-minor-mode' keymap."
44  :type 'string
45  :group 'skel)
46 
47 (defcustom skel-triggers nil
48  "Association of symbols to a specific condition which can be used
49 to trigger `skel-actions' based on the `skel-behavior' value."
50  :type '(list function)
51  :group 'skel)
52 
53 (defcustom skel-actions nil
54  "Array of actions which may be performed on skeletons."
55  :type 'obarray
56  :group 'skel)
57 
58 (defcustom skel-id-prefix "sk"
59  "Default prefix for `make-id'."
60  :type 'string
61  :group 'skel)
62 
63 (defvar-keymap skel-minor-mode-map
64  :doc "skel-minor-mode keymap."
65  :repeat (:enter)
66  :prefix 'skel-minor-mode-map)
67 
68 (define-minor-mode skel-minor-mode
69  "skel-minor-mode"
70  :global t
71  :lighter " Sk"
72  :group 'skel
73  :keymap skel-minor-mode-map
74  :version skel-version
75  (keymap-local-set skel-minor-mode-map-prefix skt-minor-mode-map))
76 
77 ;; TODO 2023-09-06:
78 (define-derived-mode skel-mode lisp-mode "SKEL"
79  :group 'skel
80  (skel-minor-mode 1))
81 
82 (defun maybe-skel-minor-mode ()
83  "Check the current environment and determine if `skel-minor-mode' should
84 be enabled. This function is added as a hook to
85 `lisp-data-mode-hook'.")
86 
87 (defvar skel-hashtable (make-hash-table :test #'equal)
88  "Internal table of available skeletons.")
89 
90 (defvar skel-stack nil "Internal stack of skeletons.")
91 
92 (defcustom skel-state 'passive
93  "State toggle for the `skel' system. Base states are passive and
94 active."
95  :type 'symbol
96  :group 'skel)
97 
98 (defvar skel-active-map nil
99  "List of cons cells of the form (SYM . BODY...) where SYM is a member of
100 `skel-triggers'.")
101 
102 (defvar skel-passive-map nil
103  "list of cons cells of the form (SYM . BODY...) where SYM is a member of
104 `skel-triggers'.")
105 
106 (defmacro make-id (&optional pre)
107  `(let ((pre ,(if-let (pre) (concat skel-id-prefix "-" pre "-") (concat skel-id-prefix "-")))
108  (current-time-list nil))
109  (symb pre (prog1 gensym-counter (setq gensym-counter (1+ gensym-counter))) (format "%x" (car (current-time))))))
110 
111 (defmacro defcmd (name &rest body) `(defun ,name nil (interactive) ,@body))
112 
113 (defclass sk (sxp)
114  ((id :initarg :id :initform (make-id)))
115  :documentation "Base class for skeleton objects. Inherits from `sxp'."
116  :abstract t)
117 
118 (defcmd sk-classes (eieio-class-children 'sk))
119 
120 (defmacro def-sk-class (name doc &optional slots superclasses)
121  "Define a new class with superclass of `skel'+SUPERCLASSES, SLOTS,
122 DOC, and NAME."
123  (declare (indent 1))
124  `(defclass ,(symb "sk-" name)
125  ,(if superclasses `(sk ,@superclasses) '(sk))
126  ,(if slots
127  `(,@slots
128  (:id :initarg :id :initform (make-id ,(symbol-name name)) :accessor id))
129  `((:id :initarg :id :initform (make-id ,(symbol-name name)) :accessor id)))
130  :documentation ,doc))
131 
132 (def-sk-class target "Target skeleton class.")
133 (def-sk-class source "Source skeleton class.")
134 (def-sk-class rule
135  "Config skeleton class."
136  ((target :initarg :target :initform nil :type (or null sk-target))
137  (rules :initarg :source :initform nil :type (or null sk-source))))
138 
139 (def-sk-class project
140  "Project skeleton class."
141  ((type :initarg :type :initform nil :accessor sk-project-type :type (or null symbol))
142  (rules :initarg :rules :initform nil :accessor sk-project-rules :type list)))
143 
144 (defun skel-init ()
145  "Initialize the skel library."
146  (interactive)
147  (add-to-list 'auto-mode-alist '("skelfile" . skel-mode))
148  (add-to-list 'auto-mode-alist '("\\.sk\\'" . skel-mode)))
149 
150 ;;; Autotype
151 ;; ref: https://raw.githubusercontent.com/xFA25E/skempo/master/skempo.el
152 
153 ;; (defun modify-lisp-syntax-tables ()
154 ;; (modify-syntax-entry ?* "w" (syntax-table))
155 ;; (modify-syntax-entry ?- "w" (syntax-table)))
156 ;; (dolist (hook '(lisp-mode-hook emacs-lisp-mode-hook))
157 ;; (add-hook hook #'modify-lisp-syntax-tables))
158 
159 (defun skt--tags-variable (mode)
160  "Return a tempo tags variable's symbol for MODE."
161  (when mode
162  (intern (replace-regexp-in-string
163  (rx "-mode" eos) "-skt-tags"
164  (symbol-name mode)))))
165 
166 (defun skt--remove-tag-list (tag-list)
167  "Remove TAG-LIST from `tempo-local-tags'."
168  (setf (alist-get tag-list tempo-local-tags nil t) nil))
169 
170 (defun skt--insert-mark (marker)
171  "Insert a MARKER to `tempo-marks' while keeping it sorted.
172 Remove duplicate marks from `tempo-marks'. Set to nil removed
173 markers. This function is used as an :override advice to
174 `tempo-insert-mark', because the original function does not
175 remove duplicate elements. Duplicate markers appear when the
176 buffer gets smaller, markers start pointing to the same location.
177 We don't want that, because a lot of useless markers can slow
178 down Emacs."
179  (if (not tempo-marks)
180  (setq tempo-marks (list marker))
181  (let ((markers tempo-marks))
182  (cond
183  ((< marker (car markers))
184  (setq tempo-marks (cons marker tempo-marks)))
185  (t
186  (while (and (cdr markers) (<= (cadr markers) marker))
187  (if (/= (car markers) (cadr markers))
188  (setq markers (cdr markers))
189  (when (markerp (cadr markers)) (set-marker (cadr markers) nil))
190  (setcdr markers (cddr markers))))
191 
192  (if (= marker (car markers))
193  (when (markerp marker) (set-marker marker nil))
194  (setcdr markers (cons marker (cdr markers))))))
195 
196  (while (cdr markers)
197  (if (/= (car markers) (cadr markers))
198  (setq markers (cdr markers))
199  (when (markerp (cadr markers)) (set-marker (cadr markers) nil))
200  (setcdr markers (cddr markers)))))))
201 
202 (defun skt-add-tag (tag template &optional tag-list)
203  "Add a TEMPLATE TAG to TAG-LIST or to `tempo-tags'.
204 It is an :override function for `tempo-add-tag'. The original
205 function does not update identical tags."
206  (interactive "sTag: \nCTemplate: ")
207  (let ((tag-list (or tag-list 'tempo-tags)))
208  (if-let ((value (assoc tag (symbol-value tag-list))))
209  (setcdr value template)
210  (set tag-list (cons (cons tag template) (symbol-value tag-list))))
211  (tempo-invalidate-collection)))
212 
213 (defun skt--list-derived-modes (mode)
214  "List all derived modes of MODE + MODE itself."
215  (let ((modes nil))
216  (while mode
217  (when-let ((alias (symbol-function mode)))
218  (when (symbolp alias)
219  (setq mode alias)))
220  (push mode modes)
221  (setq mode (get mode 'derived-mode-parent)) )
222  (nreverse modes)))
223 
224 ;;; Commands
225 (defvar-keymap skt-minor-mode-map
226  :doc "skt-minor-mode keymap."
227  :repeat (:enter)
228  :prefix 'skt-minor-mode-map
229  "a" #'edit-abbrevs)
230 
231 (define-minor-mode skt-minor-mode
232  "Minor mode for skt-templates."
233  :init-value nil
234  :lighter " Skt"
235  :keymap skt-minor-mode-map
236  (let* ((modes (skt--list-derived-modes major-mode))
237  (tag-vars (mapcar #'skt--tags-variable modes))
238  (bound-tag-vars (cl-delete-if-not #'boundp tag-vars)))
239  (if skt-minor-mode
240  (mapc #'tempo-use-tag-list bound-tag-vars)
241  (mapc #'skt--remove-tag-list bound-tag-vars))))
242 
243 (defun skt-register-auto-insert (rx template)
244  "Associate a template with a file regexp and insert into `auto-insert-alist'."
245  (cl-pushnew (cons rx template) auto-insert-alist))
246 
247 (defun skt--define-template (function-symbol body &optional docstring)
248  "Define a tempo template with BODY.
249 This will generate a function with FUNCTION-SYMBOL and
250 DOCSTRING.
251 
252 The main purpose of this function is to have a better controlled
253 alternative to `tempo-define-template'."
254  (let ((template-symbol (gensym (symbol-name function-symbol))))
255  (set template-symbol body)
256  (defalias function-symbol
257  (lambda (&optional arg)
258  (interactive "*P")
259  (tempo-insert-template template-symbol (xor tempo-insert-region arg)))
260  docstring)))
261 
262 (defun skt--define-skeleton (function-symbol body &optional docstring)
263  "Define a skeleton template with BODY.
264 This will generate a function with FUNCTION-SYMBOL and
265 DOCSTRING.
266 
267 The main purpose of this function is to have a better controlled
268 alternative to `define-skeleton', especially because it is a
269 function instead of a macro."
270  (defalias function-symbol
271  (lambda (&optional str arg)
272  (interactive "*P\nP")
273  (skeleton-proxy-new body str arg))
274  docstring))
275 
276 (defun skt--define-function (function-symbol function &optional docstring)
277  "This will generate an alias to FUNCTION with FUNCTION-SYMBOL.
278 DOCSTRING is used as a docstring to FUNCTION-SYMBOL."
279  (defalias function-symbol function docstring))
280 
281 (defun skt--mode-name (mode)
282  "Get MODE name without a -mode suffix."
283  (string-trim-right (symbol-name mode) (rx "-mode" eos)))
284 
285 (defun skt--function-name (name modes)
286  "Generate a name for a skt template function.
287 NAME and MODES are used to generate unique, but consistent
288 names."
289  (concat "skt-template-"
290  (mapconcat (lambda (mode) (concat (skt--mode-name mode) "-"))
291  (sort modes #'string<) "")
292  name))
293 
294 (defun skt--mode-abbrev-table (mode)
295  "Get abbrev table for MODE or `global-abbrev-table' if nil."
296  (if mode
297  (derived-mode-abbrev-table-name mode)
298  'global-abbrev-table))
299 
300 (defun skt--abbrev-table (mode)
301  "Get skt abbrev table for MODE."
302  (intern (concat "skt-" (symbol-name (skt--mode-abbrev-table mode)))))
303 
304 (defun skt--abbrev-table-names (table)
305  "Return abbrev TABLE names."
306  (let ((names nil))
307  (mapatoms (lambda (abbrev)
308  (when (symbol-value abbrev)
309  (push (symbol-name abbrev) names)))
310  (symbol-value table))
311  names))
312 
313 (defun skt--modes (mode)
314  "Normalize MODE argument."
315  (cond ((consp mode) mode)
316  ((null mode) nil)
317  ((symbolp mode) (list mode))))
318 
319 ;;;###autoload
320 (defun skt--define (define-function name modes tag abbrev docstring body)
321  "Define a skt template.
322 
323 DEFINE-FUNCTION is a function that takes a function symbol, BODY
324 and DOCSTRING as its arguments. It must define a new function
325 with that symbol and that docstring.
326 
327 NAME is a string used in generating a function symbol, TAG and
328 ABBREV.
329 
330 MODES is a list of modes for which TAG and ABBREV will be
331 created. If it's nil, TAG and ABBREV will be generated
332 globally.
333 
334 TAG/ABBREV is a boolean, which indicates whether a tag/abbrev
335 must be created for this template.
336 
337 DOCSTRING is a string (or nil) which will be supplied to
338 DEFINE-FUNCTION.
339 
340 BODY is an arbitrary argument passed to DEFINE-FUNCTION."
341  (let* ((function-symbol (intern (skt--function-name name modes)))
342  (modes (or modes '(nil))))
343  (funcall define-function function-symbol body docstring)
344  (put function-symbol 'no-self-insert t)
345 
346  (when tag
347  (let ((tag-symbol (gensym (symbol-name function-symbol))))
348  (if (eq #'skt--define-template define-function)
349  (set tag-symbol body)
350  (set tag-symbol `((ignore (,function-symbol)))))
351  (dolist (mode modes)
352  (let ((var (skt--tags-variable mode)))
353  (unless (boundp var)
354  (set var nil))
355  (tempo-add-tag name tag-symbol var)))
356  (dolist (buffer (buffer-list))
357  (with-current-buffer buffer
358  (when (and (or (equal '(nil) modes) (apply #'derived-mode-p modes))
359  skt-minor-mode)
360  (skt-minor-mode -1)
361  (skt-minor-mode 1))))))
362 
363  (when abbrev
364  (dolist (mode modes)
365  (let ((mode-table (skt--mode-abbrev-table mode))
366  (table (skt--abbrev-table mode)))
367  (define-abbrev-table mode-table nil)
368  (define-abbrev-table table nil :case-fixed t :skt t)
369  (define-abbrev (symbol-value table) name "" function-symbol
370  :case-fixed t :system t :skt t)
371 
372  (let* ((names (skt--abbrev-table-names table))
373  (regexp (concat (regexp-opt names "\\_<\\(") " *")))
374  (abbrev-table-put (symbol-value table) :regexp regexp))
375 
376  (let ((parents (abbrev-table-get (symbol-value mode-table) :parents)))
377  (cl-pushnew (symbol-value table) parents :test #'eq)
378  (abbrev-table-put (symbol-value mode-table) :parents parents)))))
379 
380  function-symbol))
381 
382 ;;;###autoload
383 (cl-defmacro skt-define-template (name (&key mode tag abbrev docstring) &body body)
384  "Define a tempo template.
385 This macro defines a new tempo template or updates the old one.
386 NAME is a symbol. ARGS is a list of the form ([KEY VALUE]...)
387 where each KEY can be one of :tag, :abbrev, :docstring or :mode.
388 
389 If KEY is :tag, VALUE should be a boolean. If VALUE is non-nil,
390 then a tempo tag with NAME will be created for this template.
391 
392 If KEY is :abbrev, VALUE should be a boolean. If VALUE is
393 non-nil, then a NAME abbrev will be created for this template.
394 
395 If KEY is :docstring, VALUE should be a string. It will be a
396 docstring of the generated function.
397 
398 If KEY is :mode, VALUE should be a list of modes or single mode.
399 If this option is provided, than a tempo tag and an abbrev will
400 be created for these modes, otherwise they will be global (if
401 :tag and :abbrev options were provided, of course).
402 
403 BODY is a sequence of tempo elements that will be passed as a
404 list directly to `tempo-define-template's second argument.
405 
406 Example:
407 \(skt-define-template defvar (:mode emacs-lisp-mode :tag t :abbrev t
408  :docstring \"defvar template\")
409  \"(defvar \" (string-trim-right (buffer-name) (rx \".el\" eos)) \"-\" p n>
410  r> \")\")"
411  `(skt--define 'skt--define-template ,(symbol-name name)
412  ',(skt--modes mode) ,tag ,abbrev ,docstring ',body))
413 
414 ;;;###autoload
415 (cl-defmacro skt-define-skeleton (name (&key mode tag abbrev docstring) &rest body)
416  "Define skeleton template.
417 See `skt-define-template' for explanation of NAME, MODE, TAG,
418 ABBREV and DOCSTRING.
419 
420 BODY is a sequence of skeleton elements that will be passed
421 directly to `define-skeleton'.
422 
423 Example:
424 \(skt-define-skeleton defun (:mode (emacs-lisp-mode lisp-interaction-mode)
425  :tag t :abbrev t
426  :docstring \"defun template\")
427  \"(defun \" str \" (\" @ - \")\" \n
428  @ _ \")\" \n)"
429  `(skt--define #'skt--define-skeleton ,(symbol-name name)
430  ',(skt--modes mode) ,tag ,abbrev ,docstring ',body))
431 
432 ;;;###autoload
433 (cl-defmacro skt-define-function (name (&key mode tag abbrev docstring) function)
434  "Define FUNCTION template.
435 See `skt-define-template' for explanation of NAME, MODE, TAG,
436 ABBREV and DOCSTRING.
437 
438 The main purpose of this macro, is to create tempo tags and
439 abbrevs for existing skeleton templates, such as `sh-case'.
440 
441 Example:
442 \(skt-define-function shcase (:tag t :abbrev t :mode `sh-mode') `sh-case')"
443  `(skt--define #'skt--define-function ,(symbol-name name)
444  ',(skt--modes mode) ,tag ,abbrev ,docstring ',function))
445 
446 (defun skt--complete-template (string tag-list)
447  "An :override advice function for `tempo-display-completions'.
448 Show completion for STRING in a TAG-LIST. After selection
449 expand template.
450 
451 Rewritten because the original function uses an old way of
452 displaying completions in a separate buffer, which is not
453 clickable anyway. Now it uses new (compared to the originial
454 tempo package) and shiny `completing-read' interface."
455  (let* ((tags (mapcar #'car tag-list))
456  (tag (completing-read "Skt: " tags nil t string)))
457  (delete-char (- (length string)))
458  (tempo-insert-template (cdr (assoc tag tag-list)) nil)))
459 
460 ;;;###autoload
461 (defcustom skt-enable-tempo-elements nil
462  "Enable extra tempo elements.
463 These elements add conditionals and looping support for tempo
464 like those in skeleton, making skeleton pretty much obsolete.
465 
466 If you want to set this option from ELisp, you have to remove
467 `skt-tempo-user-elements' from `tempo-user-elements' on nil
468 and add it on non-nil."
469  :type '(boolean :tag "Enable tempo elements?")
470  :set (lambda (variable value)
471  (if value
472  (add-hook 'tempo-user-functions #'skt-tempo-user-elements)
473  (remove-hook 'tempo-user-functions #'skt-tempo-user-elements))
474  (set-default variable value))
475  :group 'skel)
476 
477 (defcustom skt-completing-read nil
478  "Override default `tempo-display-completions'.
479 By default it uses a completion buffer to show completions. This
480 option overrides this function to use `completing-read' to select
481 partial skt tag or complete tag on region.
482 
483 If you wish to set this variable from ELisp code, you have to
484 remove `skt--complete-template' advice from
485 `tempo-display-completions' on nil and add it as on :override
486 advice on non-nil."
487  :type '(boolean :tag "Override?")
488  :set (lambda (variable value)
489  (if value
490  (advice-add 'tempo-display-completions :override #'skt--complete-template)
491  (advice-remove 'tempo-display-completions #'skt--complete-template))
492  (set-default variable value))
493  :group 'skel)
494 
495 (defcustom skt-delete-duplicate-marks nil
496  "Override default `tempo-insert-mark'.
497 Marks are used to jump on points of interest in a template. By
498 default `tempo-insert-mark' does not remove duplicate marks.
499 Duplicate marks might appear when the buffer shrinks and some of
500 the marks start pointing to the same location. This option tries
501 to fix this by checking for duplicate marks every time the
502 function is called. Emacs might get slower with a lot of
503 marks.
504 
505 If you want to set this option from ELisp, you have to remove
506 `skt--insert-mark' advice from `tempo-insert-mark' on nil and
507 add it as on :override advice on non-nil."
508  :type '(boolean :tag "Override?")
509  :set (lambda (variable value)
510  (if value
511  (advice-add 'tempo-insert-mark :override #'skt--insert-mark)
512  (advice-remove 'tempo-insert-mark #'skt--insert-mark))
513  (set-default variable value))
514  :group 'skel)
515 
516 (progn
517  (put 'tempo-define-template 'lisp-indent-function 1)
518  (put 'skt-define-template 'lisp-indent-function 2)
519  (put 'skt-define-skeleton 'lisp-indent-function 2)
520  (put 'skt-define-function 'lisp-indent-function 2))
521 
522 ;;; Tempo Elements
523 (defvar skt-tempo-else-key (kbd "C-M-g")
524  "Key used to execute else branch in tempo conditional.")
525 
526 (defun skt-tempo--prompt (prompt)
527  "Make prompt for tempo conditional.
528 PROMPT is preceded with `skt-tempo-else-key'."
529  (concat "(" (key-description skt-tempo-else-key) " to quit) " prompt))
530 
531 (defun skt-tempo-user-elements (element)
532  "Support for conditional and looping tempo elements.
533 The following forms are supported for ELEMENT:
534 
535 \(:if (PROMPT VAR) THEN ELSE)
536 
537 \(:when (PROMPT VAR) BODY...)
538 
539 \(:while (PROMPT VAR) BODY...)
540 
541 PROMPT is a string used to read value for VAR. VAR is a tempo
542 variable symbol. Its value can be read with s, as usual. BODY,
543 THEN and ELSE are tempo elements. To abort the execution of
544 these elements, user must press `skt-tempo-else-key'.
545 
546 The main purpose of this extension is to mimic skeleton
547 conditionals and iterative templats. Skeleton becomes pretty
548 much obsolete with this extension."
549  (pcase element
550  (`(:if (,(and (pred stringp) prompt) ,(and (pred symbolp) var)) ,then ,else)
551  (let ((prompt (skt-tempo--prompt prompt))
552  (map (make-sparse-keymap)))
553  (set-keymap-parent map minibuffer-local-map)
554  (define-key map skt-tempo-else-key
555  (lambda () (interactive) (throw 'else else)))
556  (catch 'else
557  (tempo-save-named var (read-from-minibuffer prompt nil map))
558  then)))
559  (`(:when (,(and (pred stringp) prompt) ,(and (pred symbolp) var)) . ,body)
560  `(:if (,prompt ,var) (l ,@body) (l)))
561  (`(:while (,(and (pred stringp) prompt) ,(and (pred symbolp) var)) . ,body)
562  `(:when (,prompt ,var) ,@body ,element))))
563 
564 (provide 'skel)
565 (provide 'sk)
566 ;;; sk.el ends here