Emacs Lisp: Abbrev, Template, for Major Mode

By Xah Lee. Date: . Last updated: .

This page shows you how to create abbrev and function templates for your own major mode.

Problem

You are writing a major mode for your own language. You want function templates for your language.

For example, when user types “d”, you want it to expand to:

(defun f▮ ()
  "DOCSTRING"
  (interactive)
  (let (VAR)

  ))

(We will be using emacs lisp as example. But the abbrev string and expansion string can be any string for any language.)

Solution

Here's the complete code for a major mode with abbrev.

;; when your package is still in beta stage, it's best to clear abbrev table first. by setting it to nil
(setq xyz-abbrev-table nil)

(define-abbrev-table 'xyz-abbrev-table
  '(
    ("d" "(defun f▮ ()\n  \"DOCSTRING\"\n  (interactive)\n  (let (VAR)\n\n  ))" )
    ("m" "(message \"%s▮\" ARGS)" )
    ("s" "(setq ▮ VAL)" )
    ;; hundreds more
    )
  "Abbrev table for `xyz'"
  )

(abbrev-table-put xyz-abbrev-table :regexp "\\([_-*0-9A-Za-z]+\\)")
(abbrev-table-put xyz-abbrev-table :case-fixed t)
(abbrev-table-put xyz-abbrev-table :system t)

(define-derived-mode xyz prog-mode "∑lisp"
  "A major mode for emacs lisp...."

  (abbrev-mode 1)

  :abbrev-table xyz-abbrev-table ; actually, we don't need this line, because our name is “xyz” + “-abbrev-table” so define-derived-mode will find it and set for us
  )
  1. Copy and paste the code into a file. Save it as “xyz.el”
  2. Open “xyz.el”, Alt+x eval-buffer.
  3. Open a new file, then Alt+x xyz
  4. Now, type “s”, then type space. It'll become “(setq ▮ VAL)”

(We use “xyz-” as the mode's variable/function prefix. It doesn't mean anything.)

Here's how it works.

First, define the abbrev table, like this

;; if still beta, it's convenient to start fresh
(setq xyz-abbrev-table nil)

(define-abbrev-table 'xyz-abbrev-table
  '(("d" "(defun f▮ ()\n  \"DOCSTRING\"\n  (interactive)\n  (let (VAR)\n\n  ))")
    ("m" "(message \"%s▮\" ARGS)")
    ("s" "(setq ▮ VAL)")
    ;; hundreds more
    )
  "Abbrev table for `xyz'"
  )

Or, you can define each abbrev separately.

For example, let's add one for “lambda”.

(define-abbrev xyz-abbrev-table "lambda" "(lambda (x▮) (interactive) BODY)" )
define-abbrev-table
Defines whole bunch of abbrevs, by creating a abbrev table first then call define-abbrev for each entry. (info "(elisp) Abbrev Tables")
define-abbrev
Adds one abbrev definition to a existing abbrev table. (info "(elisp) Defining Abbrevs")

Now we set properties to the table.

(abbrev-table-put xyz-abbrev-table :regexp "\\([_-*0-9A-Za-z]+\\)")
(abbrev-table-put xyz-abbrev-table :case-fixed t)
(abbrev-table-put xyz-abbrev-table :system t)

Each abbrev can have several properties. Example:

:enable-function
a function that determines whether it should be expanded at the time. Value should be a symbol.
:case-fixed
t or nil. Whether letter case sensitive.
:regexp
regex to determine how far back to grab as the abbrev symbol. By default, it only get character that has word syntax (by default, word syntax means letters and 0 to 9, but not dash - or lowline _.). [see Emacs Lisp: Syntax Table]
:system
if t, “system abbrev”, means don't save to user's abbrev file. Major mode's abbrev should be system abbrev.
:parents
A list of parent abbrev tables. Value should be a list, each element should be a variable, not symbol. For example, (list lisp-mode-abbrev-table)

You can attach each of these properties for each abbrev individually, or, you can attach any of these properties to a abbrev table for all abbrevs in the table.

(info "(elisp) Abbrev Table Properties")

In your mode activation command, you need to tell it to use your abbrev table as local.

;; set local abbrev table
(setq local-abbrev-table xyz-abbrev-table)

If you are using define-derived-mode to create a major mode, you can set the property :abbrev-table table_var_name. Like this:

(define-derived-mode xyz prog-mode "∑lisp"
  "A major mode for emacs lisp...."

  (abbrev-mode 1)

  :abbrev-table xyz-abbrev-table
  ;; actually we don't need this line, since our abbrev table name is "xyz" followed by “-abbrev-table”, meaning, define-derived-mode will automatically find it and set it as local abbrev table

  )

Or, just name your abbrev table to be your mode activation command name joined with “-abbrev-table”. For example, our mode activation command name is “xyz”, and our abbrev table variable is named “xyz-abbrev-table”, and define-derived-mode will automatically use that table as local abbrev.

Advanced, Create Function Templates

Here, we'll use abbrev system to build a function template such that:

  1. Not expand when inside string quote or comment.
  2. Add parenthesis as appropriate.
  3. Move cursor into the proper position after expansion.
  4. Do not add the trigger character, such as space.

Here's the complete code.

(defun xe2-abbrev-enable-function ()
  "Return t if not in string or comment. Else nil.
This is for abbrev table property `:enable-function'.
Version 2016-10-24"
  (let (($syntax-state (syntax-ppss)))
    (not (or (nth 3 $syntax-state) (nth 4 $syntax-state)))))

(defun xe2-expand-abbrev ()
  "Expand the symbol before cursor,
if cursor is not in string or comment.
Returns the abbrev symbol if there's a expansion, else nil.
Version 2016-10-24"
  (interactive)
  (when (xe2-abbrev-enable-function) ; abbrev property :enable-function is ignored, if you define your own expansion function
    (let (
          $p1 $p2
          $abrStr
          $abrSymbol
          )
      (save-excursion
        (forward-symbol -1)
        (setq $p1 (point))
        (forward-symbol 1)
        (setq $p2 (point)))
      (setq $abrStr (buffer-substring-no-properties $p1 $p2))
      (setq $abrSymbol (abbrev-symbol $abrStr))
      (if $abrSymbol
          (progn
            (abbrev-insert $abrSymbol $abrStr $p1 $p2 )
            (xe2--abbrev-position-cursor $p1)
            $abrSymbol)
        nil))))

(defun xe2--abbrev-position-cursor (&optional @pos)
  "Move cursor back to ▮ if exist, else put at end.
Return true if found, else false.
Version 2016-11-05"
  (interactive)
  (message "pos is %s" @pos)
  (let (($found-p (search-backward "▮" (if @pos @pos (max (point-min) (- (point) 100))) t )))
    (when $found-p (delete-char 1))
    $found-p
    ))

(defun xe2--ahf ()
  "function to run after abbrev expansion
 Mostly used to prevent inserting the char that triggered expansion
 the “ahf” stand for abbrev hook function.
Version 2016-10-24"
  (interactive)
  (message "abbrev hook function ran" )
  t)

(put 'xe2--ahf 'no-self-insert t)

;; if still dev, it's convenient to start fresh
(setq xe2-abbrev-table nil)

(define-abbrev-table 'xe2-abbrev-table
  '(

    ("d" "(defun f▮ ()\n  \"DOCSTRING\"\n  (interactive)\n  (let (VAR)\n\n  ))" xe2--ahf)
    ("i" "(insert ▮)" xe2--ahf)
    ("l" "(let (x▮)\n x\n)" xe2--ahf)
    ("m" "(message \"%s▮\" ARGS)" xe2--ahf)
    ("p" "(point)" xe2--ahf)
    ("s" "(setq ▮ VAL)" xe2--ahf)
    ("w" "(when ▮)" xe2--ahf)

    ("bsnp" "(buffer-substring-no-properties START▮ END)" xe2--ahf)

    ;; hundreds more
    )
  "Abbrev table for `xe2'"
  )

(abbrev-table-put xe2-abbrev-table :regexp "\\([_-*0-9A-Za-z]+\\)")
(abbrev-table-put xe2-abbrev-table :case-fixed t)
(abbrev-table-put xe2-abbrev-table :system t)
(abbrev-table-put xe2-abbrev-table :enable-function 'xe2-abbrev-enable-function)

(define-derived-mode xe2 prog-mode "∑xe2"
  "A major mode for emacs lisp...."

  (make-local-variable 'abbrev-expand-function)
  (if (or
       (and (>= emacs-major-version 24)
            (>= emacs-minor-version 4))
       (>= emacs-major-version 25))
      (progn
        (setq abbrev-expand-function 'xe2-expand-abbrev))
    (progn (add-hook 'abbrev-expand-functions 'xe2-expand-abbrev nil t)))

  (abbrev-mode 1)

  ;; no need in our case.
  ;; :abbrev-table xe2-abbrev-table
  )

How Abbrev Works

Abbrev Trigger Character

The trigger is activated if the char before cursor is word syntax and char just typed is not. ((info "(elisp) Syntax Tables")) Normally it's space or return, or a punctuation char.

The trigger character cannot be changed. This is low level, implemented as part of self-insert-command. (see its C source code by Alt+x describe-function)

Abbrev Enable Function

Then, the :enable-function property for the abbrev is consulted, to decide whether to expand. The value should be a function that returns true or false. If it returns true, do expand, else do nothing.

;; adding a property to abbrev table
(abbrev-table-put xe2-abbrev-table :enable-function 'xe2-abbrev-enable-function)

Our enable function is this:

(defun xe2-abbrev-enable-function ()
  "Return t if not in string or comment. Else nil.
This is for abbrev table property `:enable-function'.
Version 2016-10-24"
  (let (($syntax-state (syntax-ppss)))
    (not (or (nth 3 $syntax-state) (nth 4 $syntax-state)))))

It expand only if cursor is not in string or comment.

Abbrev Expand Function

Then, the value of abbrev-expand-function variable is called. That function is for expanding the abbrev, but can do anything you want.

By default, the value of abbrev-expand-function is abbrev--default-expand.

If you want your own expansion function, in your major mode activation function, you need to set the variable abbrev-expand-function as local var, and with value to your own expansion function. See the code example in define-derived-mode above.

We have a example of the expansion function xe2-expand-abbrev in our mode above. It expands the abbrev, but also move cursor into desired place after expansion. e.g. “w” expands to “(when ▮)” and also move cursor inside the parenthesis.

Note: If you define your own expansion function, then the “:enable-function” property value is ignored. If you still want a function to check when should abbrev be expanded, you need to put that check inside your expansion function.

The actual expansion, of getting a abbrev string's expanded string, is by calling the function abbrev-insert, like this:

(abbrev-insert ABBREV &optional NAME WORDSTART WORDEND)

There are several other functions that help you write the expansion, such as what's the last abbrev used. see (info "(elisp) Abbrev Expansion")

The return value of the expansion function should be non-nil if abbrev is considered expanded. Else, nil.

If non-nil, and return value is actually the abbrev symbol, then the hook for that abbrev symbol is called. The hook function is defined as part of each abbrev definition.

For example, in our abbrev for “insert” ("i" "(insert ▮)" xe2--ahf) we have a hook named The xe2--ahf. (we named it “--ahf” as a reminder of being “abbrev hook function”)

Our hook xe2--ahf simply returns t.

If this hook function has property (put 'xe2--ahf 'no-self-insert t), and if it returns non-nil, then the char that triggered the abbrev will not be inserted. (info "(elisp) Defining Abbrevs")

That ends the abbrev expansion process.

(2016-11-05 thanks to John Kitchen ( [2016-11-05 https://twitter.com/johnkitchin ] ) for suggesting to remove the cursor position place holder char ▮ after expanding a abbrev.)

Emacs: Abbrev Mode


Elisp Write Major Mode

Basics

Package Name/Load

Syntax Table