Elisp: Create Abbrev and Templates 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)

  ))

When user types “bsnp”, it expands to

(buffer-substring-no-properties STARTEND)

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

Solution

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

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

(define-abbrev-table 'xem-abbrev-table
  '(
    ("d" "(defun f▮ ()\n  \"DOCSTRING\"\n  (interactive)\n  (let (VAR)\n\n  ))" )
    ("i" "(insert ▮)" )
    ("l" "(let (x▮)\n x\n)" )
    ("m" "(message \"%s▮\" ARGS)" )
    ("p" "(point)" )
    ("s" "(setq ▮ VAL)" )
    ("w" "(when ▮)" )
    ("bsnp" "(buffer-substring-no-properties START▮ END)" )
    ;; hundreds more
    )
  "Abbrev table for `xem'"
  )

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

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

  (abbrev-mode 1)

  :abbrev-table xem-abbrev-table ; actually, we don't need this line, because our name is “xem” + “-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 “xem.el”
  2. Open “xem.el”, Alt+x eval-buffer.
  3. Open a new file, then M-x xem
  4. Now, type “p”, then type space. It'll become “(point)”

(We use “xem-” as the mode's variable/function prefix. xem stand for x-elisp-mode. It can be anything for your mode, such as “xyz-mode-”)

Here's how it works.

First, define the abbrev table, like this

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

(define-abbrev-table 'xem-abbrev-table
  '(
    ("d" "(defun f▮ ()\n  \"DOCSTRING\"\n  (interactive)\n  (let (VAR)\n\n  ))" )
    ("i" "(insert ▮)" )
    ("l" "(let (x▮)\n x\n)" )
    ("m" "(message \"%s▮\" ARGS)" )
    ("p" "(point)" )
    ("s" "(setq ▮ VAL)" )
    ("w" "(when ▮)" )
    ("bsnp" "(buffer-substring-no-properties START▮ END)" )
    ;; hundreds more
    )
  "Abbrev table for `xem'"
  )

Or, you can define each abbrev separately.

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

(define-abbrev xem-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.

define-abbrev adds one abbrev definition to a existing abbrev table.

(info "(elisp) Abbrev Tables")

(info "(elisp) Defining Abbrevs")

Now we set properties to the table.

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

Each abbrev can have several properties. e.g.

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 xem-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 xem prog-mode "∑lisp"
  "A major mode for emacs lisp...."

  (abbrev-mode 1)

  ;; no need in our case.
  ;; :abbrev-table xem-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 “xem”, and our abbrev table variable is named “xem-abbrev-table”, and define-derived-mode will automatically use that table as local abbrev.

Advanced 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

First, the trigger character. This is low level, cannot be changed. You can see it in C source code for self-insert-command. 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.

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.

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 expand 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 ( https://twitter.com/johnkitchin ) for suggesting to remove the cursor position place holder char ▮ after expanding a abbrev.)

Writing Major Mode

  1. How to Write a Emacs Major Mode for Syntax Coloring
  2. Elisp: html6-mode
  3. Elisp: Font Lock Mode Basics
  4. Elisp: How to Define Face
  5. Elisp: How to Color Comment in Major Mode
  6. Elisp: How to Write Comment Command in Major Mode
  7. Elisp: How to Write Your Own Comment Command from Scratch
  8. Elisp: How to Write Keyword Completion Command
  9. Elisp: How to Create Keymap for Major Mode
  10. Elisp: Create Abbrev and Templates for Major Mode
  11. Elisp: Text Properties
  12. Elisp: Overlay Highlighting
  13. Emacs: Lookup Google, Dictionary, Documentation
  14. Elisp: Syntax Table Tutorial

  1. Elisp: How to Name Your Major Mode
  2. Elisp: What's “feature”?
  3. Elisp: require, load, load-file, autoload, feature, Explained

Syntax Table

  1. Elisp: Syntax Table Tutorial
  2. Elisp: How to Find Syntax of a Character?
  3. Elisp: How to Modify Syntax Table Temporarily
  4. Elisp: How to Determine If Cursor is Inside String or Comment
  5. Elisp: Regex Patterns and Syntax Table
  6. Elisp: Find Matching Bracket Character
Liket it? Put $1 at patreon. Or Buy Xah Emacs Tutorial. Thanks.