/skempo

Enhance Emacs skeleton, tempo and abbrev.

Primary LanguageEmacs LispGNU General Public License v3.0GPL-3.0

Skempo

This is an attempt to improve Emacs built-in skeleton and tempo templates. It tries to make a unified syntax for template definitions. Adds tags and marks support for skeleton and abbrev support for tempo.

Every symbol is well documented. See Commentary and documentation strings.

Usage

Define templates

;; Multiple modes!
(skempo-define-tempo let (:tag t :abbrev t :mode (emacs-lisp-mode lisp-mode))
  "(let ((" p "))" n> r> ")")

;; Skeletons too! With mark jumping!
(skempo-define-skeleton defun (:tag t :abbrev t :mode emacs-lisp-mode)
  "Name: "
  "(defun " str " (" @ - ")" \n
  @ _ ")" \n)

;; Clever tempo templates!
(skempo-define-tempo defvar (:tag t :abbrev t :mode emacs-lisp-mode)
  "(defvar " (string-trim-right (buffer-name) (rx ".el" eos)) "-" p n>
  r> ")")

;; Define tags and abbrevs for existing skeletons and tempo templates!
(skempo-define-function shcase (:tag t :abbrev t :mode sh-mode)
  sh-case)

;; This will override emacs-lisp's "defvar", but you can always call it by
;; function name (or by tag/abbrev if they were defined).
(skempo-define-tempo defvar (:tag t :mode lisp-interaction-mode)
  "(defvar var" p n> r> ")")

Call templates

(add-hook 'emacs-lisp-mode 'skempo-mode)
(add-hook 'lisp-interaction-mode 'skempo-mode)

(with-eval-after-load 'skempo
  (easy-mmode-define-keymap
   '(("\C-z" . skempo-complete-tag-or-call-on-region)
     ("\M-g\M-e" . skempo-forward-mark)
     ("\M-g\M-a" . skempo-backward-mark))
   nil skempo-mode-map))

(custom-set-variables
 '(skempo-completing-read t)
 '(skempo-delete-duplicate-marks t)
 '(skempo-update-identical-tags t)
 '(skempo-skeleton-marks-support t)
 '(skempo-mode-lighter " Sk")
 '(skempo-enable-tempo-elements t))

What is :tag anyway?

Tempo has a concept of a tag. It is a word that triggers the expansion of a template with tempo-complete-tag. It is similar to the expansion name in Yasnippet.

Possible reasons to use it over Yasnippet?

  • Define templates in e-lisp, as opposed to some external file.
  • Emacs has an excellent reader, there is no need for new language.
  • You like built-in packages.

Additional tempo elements

This package provides skempo-tempo-user-elements function which can be enabled with skempo-enable-tempo-elements option. It adds conditional and looping constructs similar to skeleton ones, making skeleton pretty much obsolete. For example, the following snippets are equivalent:

(skempo-define-skeleton someskel ()
  nil
  "(:option"
  ;; insert until empty string is given
  ("Insert symbol: " " #:" str)
  ")")

(skempo-define-tempo sometemp ()
  "(:option"
  ;; insert until C-M-g key is pressed
  (:while ("Insert symbol: " sym)
          " #:" (s sym))
  ")")

The main difference is in the abortion. Skeleton aborts on empty string. I think that empty input is necessary, sometimes, so I choose the approach of binding a dedicate key for abortion. This key is displayed in the prompt and can be customized with skempo-tempo-else-key.

Some more complicated behavior:

(skempo-define-skeleton someskel ()
  nil
  "(:option"
  ;; insert until empty string is given
  ("Insert symbol: " " #:" str)
  & ")"
  ;; or don't insert at all.  It is a hack that physically removes "(:option"
  ;; string by deleting 8 characters if previous statements didn't move the
  ;; point
  | -8)

(skempo-define-tempo sometemp ()
  ;; If input was not aborted, start inserting
  (:when ("Insert symbol: " sym)
         "(:option #:" (s sym)
         ;; Continue inserting until C-M-g is pressed
         (:while ("Insert symbol: " sym)
                 " #:" (s sym))
         ")"))

There is also an :if element, that can execute else branch if input was aborted.

(skempo-define-tempo sometemp ()
  (:if ("Insert symbol: " sym)
       ;; Use l element to group elements together
       (l "insert " (s sym))
       "something else"))

Problems with abbrev expansion in lisp modes

You might have problems with expansion on non word characters like * or - in lisp modes. For example let* might trigger let expansion. There are some possible solutions with trade-offs.

Disable expansion on non space characters for lisp modes

(let ((enable-fn (lambda () (or (eq this-command 'expand-abbrev)
                                (eql ?\s last-command-event)))))
  (dolist (mode '(lisp-mode emacs-lisp-mode))
    (let ((table (symbol-value (skempo--abbrev-table mode))))
      (abbrev-table-put table :enable-function enable-fn))))

This solution will disable automatic expansion on, for example, let* template.

Modify syntax for these characters to make them words

(defun modify-lisp-syntax-tables ()
  (modify-syntax-entry ?* "w" (syntax-table))
  (modify-syntax-entry ?- "w" (syntax-table)))

(dolist (hook '(lisp-mode-hook emacs-lisp-mode-hook))
  (add-hook hook #'modify-lisp-syntax-tables))

This solution will treat some-long-symbol* as a single word. You can change only * character, but in that case let- will trigger let.

Bind expand-abbrev on some key

https://www.emacswiki.org/emacs/AbbrevMode#h5o-11