The “Real Elisp” series is designed to tell you about my experience with customizing Emacs using Elisp, and to invite comments from the Emacs community — if there are any Emacs users out there.

Emacs org-mode uses a markup language called ORG, and like most markup languages, it supports unordered lists and checklists — the former prefixed by – (a hyphen, a space), the latter prefixed by – [] or – [x] (more square brackets than unordered lists and the middle letter X)

In addition, org-mode provides m-ret for quickly inserting a new line (i.e., hold Alt and press Enter) for editing both lists. If the cursor is in an unordered list, the new line is automatically prefixed with -. Unfortunately, if the cursor is in the checklist, the new line does not automatically insert a pair of square brackets

It is quite tedious to manually type in [] every time. The good news is that this is Emacs, which is extensible and customizable. With a few lines of code, Emacs can type the square brackets for you.

AOP features of Emacsadvice-add

Using Emacs’ describe-key functionality, you can see that when m-ret is pressed in an org-mode file, Emacs calls the org-insert-item function. To make m-RET append square brackets automatically, you can immediately think of a simple and crude way:

  • Define a new function and setM-RETBind to it;
  • To redefine theorg-insert-itemFunction to append square brackets;

Either of these, however, requires the re-implementation of existing functions for inserting hyphens and space prefixes. A more modest way to extend the behavior of the existing org-insert-item is the Advice feature of Emacs.

Advice is one of the section-oriented programming paradigms that uses Emacs’ advice-add function to do incidental things — such as append a pair of square brackets — before or after a normal function is called. Advice-add :before and :after can be used for these two occasions, respectively, but they are not appropriate here because:

  • Check if it is in the checklist and needs to be calledorg-insert-itemBefore do;
  • To append a pair of square brackets, theorg-insert-itemAfter done.

Therefore, the correct approach is to use :around to modify the original org-insert-item function

(cl-defun lt-around-org-insert-item (oldfunction &rest args)
  "Append [] wisely after a call to org-insert-item."
  (let ((is-checkbox nil)
        (line (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
    ;; Check whether the current row is checkbox
    (when (string-match-p "- \ \ [. \ \]" line)
      (setf is-checkbox t))
    ;; Continue to insert text using the original org-insert-item
    (apply oldfunction args)
    ;; Decide whether to append the "[]" string
    (when is-checkbox
      (insert "[]"))))

(advice-add 'org-insert-item :around #'lt-around-org-insert-item)
Copy the code

M-ret now treats checklists equally

The Common Lispmethod combination

Advice-add :after, :around, and :before have identical equivalents in Common Lisp, but instead of using a function called advice-add, they are fed to a macro called defMethod. For example, using defMethod you can define a polymorphic len function that performs different logic for different types of input arguments

(defgeneric len (x(a))defmethod len ((x string))
  (length x))

(defmethod len ((x hash-table))
  (hash-table-count x))
Copy the code

Then define the corresponding :after, :around, and :before methods for the specialized version of the argument type string

(defmethod len :after ((x string))
  (format t "after len~%"(a))defmethod len :around ((x string))
  (format t "Around ~% before len is called")
  (prog1
      (call-next-method)
    (format t "After len is called around ~%")))

(defmethod len :before ((x string))
  (format t "before len~%"))
Copy the code

The rules for calling this series of methods are:

  1. First call:aroundA method of modification;
  2. Because the above method is calledcall-next-method, so call again:beforeA method of modification;
  3. Call an undecorated method (in CL this is calledprimaryMethods);
  4. Call again:afterA method of modification;
  5. And finally, it’s back to:aroundIn the callcall-next-methodThe location of the.

At first glance, Emacs’ advice-add supports far more modifiers than it actually does. In CL, :after, :around, and :before are all part of a method combination called Standard, and CL has other method combinations built into it. In the Other Method Column section, the author demonstrates the use of progn and list as an example.

If you want to emulate the other modifiers supported by Emacs’ advice-add, you must define a new Method Combination.

Programmable programming languagesdefine-method-combination

I used to think that defMethod could only accept :after, :around, and :before as features that had to be supported at the language level. It wasn’t until I stumbled into LispWorks’ define-Method-Combination entry that I realized they were just three modifiers.

(define-method-combination standard ()
  ((around (:around(a))before (:before(a))primary(a):required t)
   (after (:after)))
  (flet ((call-methods (methods)
           (mapcar #'(lambda (method)
                       `(call-method ,method))
                   methods)))
    (let ((form (if (or before after (rest primary))
                    `(multiple-value-prog1
                         (progn ,@(call-methods before)
                                (call-method ,(first primary)
                                             ,(rest primary)))
                       ,@(call-methods (reverse after)))
                    `(call-method ,(first primary)))))
      (if around
          `(call-method ,(first around)
                        (,@(rest around)
                           (make-method ,form)))
          form))))
Copy the code

In keeping with the principle of “pick a soft persimmon”, let me try to mimic the effects of advice-add :after-while and :before-while.

The effects of :after-while and :before-while are easy to understand

Call function after the old function and only if the old function returned non-nil.

Call function before the old function and don’t call the old function if function returns nil.

Therefore, when a form is created by define-method-combination (remember how Umbrella translated it into form in PCL), it must:

  • Check if there are any:before-whileA method of modification;
  • If so, check whether the call was made:before-whileWhether the return value after the modified method isNIL;
  • If not, or by:before-whileThe return value of the modified method is notNIL, we will know how to callprimaryMethods;
  • If you have been:after-whileThe method of modification, andprimaryMethod does not return a value ofNIL, these methods are called;
  • returnprimaryMethod return value.

For simplicity, although the after-while and before-while variables point to multiple “callable” methods, only the “most concrete” one is called here.

Given the name emacs-Advice, the implementation of this new Method combination is a natural step

(define-method-combination emacs-advice ()
  ((after-while (:after-while(a))before-while (:before-while(a))primary(a):required t(a))let ((after-while-fn (first after-while))
        (before-while-fn (first before-while))
        (result (gensym)))
    `(let ((,result (when ,before-while-fn
                      (call-method ,before-while-fn))))
       (when (or (null ,before-while-fn)
                 ,result)
         (let ((,result (call-method ,(first primary))))
           (when (and ,result ,after-while-fn)
             (call-method ,after-while-fn))
           ,result)))))
Copy the code

Call-method (and its partner, make-method) are macros dedicated to calling incoming methods in define-method-combination.

Verify this with a series of foobar methods

(defgeneric foobar (x)
  (:method-combination emacs-advice))

(defmethod foobar (x)
  'hello)

(defmethod foobar :after-while (x)
  (declare (ignorable x))
  (format t "for side effect~%"(a))defmethod foobar :before-while (x)
  (evenp x))

(foobar 1) ;; Returns NIL
(foobar 2) ;; Print "fo Side effect" and return HELLO
Copy the code

Afterword.

As much as I appreciate CL, the more I think about define-method-combination, the more I realize that there are limits to what a programming language can do unless it goes beyond it. For example :filter-args and :filter-return, supported by Emacs advice-add, cannot be elegantly implemented with define method-combination — not completely, You just need to combine them in a method decorated with :around.

Read the original