Emacs Lisp: Create Abbrevs / Function 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.

suppose you want these abbrevs:

Solution

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

;; -*- coding: utf-8; lexical-binding: t; -*-

;; sample major mode yy with abbrevs

;; clear value and all properties
;; useful during dev.
;; because this get rid of abbrev table symbol properties too
(setq yy-abbrev-table nil)

(define-abbrev-table 'yy-abbrev-table
  '(("fn" "function")
    ("function" "function name (x) { return x; }")
    ;; hundreds more
    )
  "Abbrev table for `yy'"
  :case-fixed t
  :system t
  )

(define-derived-mode yy prog-mode "yy"
  "A major mode for yy."

  (abbrev-mode 1)

  ;; (setq local-abbrev-table yy-abbrev-table)
  ;; normally, use this to set the local abbrev table

  ;; :abbrev-table yy-abbrev-table
  ;; this this same as
  ;; (setq local-abbrev-table yy-abbrev-table)

  ;; we don't need to manually set local-abbrev-table
  ;; because our table name has a standard naming of
  ;; mode-activation-command-name followed by -abbrev-table
  ;; so define-derived-mode will find it and set for us
  )
  1. Copy and paste the code into a buffer.
  2. Alt+x eval-buffer
  3. Open a new file, then Alt+x yy
  4. Now, type “fn”, then type space. It'll become “function”

Define Abbrev Table

There are two ways to setup abbrevs.

  1. Use define-abbrev-table.
  2. Create a abbrev table by make-abbrev-table, then use define-abbrev for each abbrev.
define-abbrev-table

(define-abbrev-table TABLENAME DEFINITIONS &optional DOCSTRING &rest PROPS)

Create a abbrev table and define a list of abbrevs.

DEFINITIONS is a list. Each element is a pair. (Each element is passed to define-abbrev.)

;; -*- coding: utf-8; lexical-binding: t; -*-

;; sample mode yy with abbrevs

(setq yy-abbrev-table nil)

(define-abbrev-table 'yy-abbrev-table
  '(
   ("fn" "function")
   ("function" "function name (x) { return x; }"))
  "Abbrev table for `yy'"
  :case-fixed t
  :system t
  )

(define-derived-mode yy prog-mode "yy"
  "A major mode for yy."
  (abbrev-mode 1))
make-abbrev-table

(make-abbrev-table &optional PROPS)

Create an abbrev table.

(setq yy-abbrev-table (make-abbrev-table ))
define-abbrev

(define-abbrev TABLE ABBREV EXPANSION &optional HOOK &rest PROPS)

Adds one abbrev definition to a existing abbrev table.

(defvar yy-abbrev-table nil "Abbrev table for `yy'")

(setq yy-abbrev-table (make-abbrev-table ))
(abbrev-table-put yy-abbrev-table :case-fixed t)
(abbrev-table-put yy-abbrev-table :system t)

(define-abbrev yy-abbrev-table "fn" "function")
(define-abbrev yy-abbrev-table "function" "function name (x) { return x; }" )

(define-derived-mode yy prog-mode "yy"
  "A major mode for yy lang."
  (abbrev-mode 1))

Abbrev Properties

how to attach properties:

Note: these properties are not the normal Symbol Property List. Both the abbrev table, and each abbrev, has their own implementation of property list. You use special function to acces them, example:

Properties for Each Abbrev

Here are the most important properties for each abbrev.

:system

if t, “system abbrev”, means don't save to user's abbrev file. Major mode's abbrev should be system abbrev.

:enable-function

a function that determines whether it should be expanded at the time. Value should be a function.

:case-fixed

t or nil. Whether letter case sensitive.

Properties for an Abbrev Table

Here are the most important properties specific for abbrev table.

:regexp

regex to determine how far back to grab as the abbrev symbol.

By default, it only get characters that have word syntax (by default, word syntax means letters and 0 to 9, but not dash - or lowline _.). [see Emacs Lisp: Syntax Table]

🛑 WARNING: 2023-08-20 i cannot get this to work. but, it's probably better to limit abbrev to just letters and digits, because that makes it easier to type.

:parents

A list of parent abbrev tables. Value should be a list, each element should be a variable, not quoted symbol, e.g. (list lisp-mode-abbrev-table)

you can set properties manually:

(abbrev-table-put yy-abbrev-table :case-fixed t)
(abbrev-table-put yy-abbrev-table :system t)

Buffer Local Abbrev Table

Each buffer can have its own abbrev table.

local-abbrev-table

Variable. Local abbrev table of current buffer.

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

If you are using define-derived-mode to create a major mode, you can use the optional argument

:abbrev-table table_var_name

to set a value for local-abbrev-table.

(define-derived-mode yy prog-mode "yy"
  "A major mode for emacs yy"
  (abbrev-mode 1)
  :abbrev-table yy-abbrev-table
  )

Or, just name your abbrev table to be your mode activation command name joined with -abbrev-table. The define-derived-mode will recognize the name and automatically set it as local abbrev.

Abbrev Trigger Character

The trigger is activated if the char before cursor is word syntax and char just typed is not. [see Emacs Lisp: Syntax Table] 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

when an abbrev expansion is triggered, 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.

;; example of defining own :enable-function

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

(defun yy-abbrev-enable-function ()
  "Return t if not in string or comment. Else nil."
  (let ((xsyntax-state (syntax-ppss)))
    (not (or (nth 3 xsyntax-state) (nth 4 xsyntax-state)))))

Abbrev Expand Function (Customize Abbrev Behavior)

An abbrev can expand to arbitrary dynamic text at expansion time, by writing your own function.

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.

abbrev-expand-function

Variable. Value is a function. This function is called to expand abbrev.

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

The default value is abbrev--default-expand.

(make-local-variable 'abbrev-expand-function)
(setq abbrev-expand-function 'yy-expand-abbrev)
;; example of defining own abbrev-expand-function

(make-local-variable 'abbrev-expand-function)
(setq abbrev-expand-function 'yy-expand-abbrev)

(defun yy-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 (yy-abbrev-enable-function)
    ;; abbrev property :enable-function is ignored, if you define your own expansion function
    (let (xp1 xp2 xabrStr xabrSymbol)
      (save-excursion
        (forward-symbol -1)
        (setq xp1 (point))
        (forward-symbol 1)
        (setq xp2 (point)))
      (setq xabrStr (buffer-substring-no-properties xp1 xp2))
      (setq xabrSymbol (abbrev-symbol xabrStr))
      (if xabrSymbol
          (progn
            (abbrev-insert xabrSymbol xabrStr xp1 xp2)
            (yy--abbrev-position-cursor xp1)
            xabrSymbol)
        nil))))

(defun yy--abbrev-position-cursor (&optional Pos)
  "Move cursor back to ▮ if exist.
Pos is a buffer position limit of search backward. If nil, default to point minus 100.
Return point if found, else nil.
Version: 2016-10-24 2023-08-20"
  (interactive)
  (let ((xfoundQ (search-backward "▮" (if Pos Pos (max (point-min) (- (point) 100))) t)))
    (when xfoundQ (delete-char 1))
    xfoundQ
    ))

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.

Abbrev's Hook

The last step of abbrev expansion is a hook function for the abbrev.

The hook can be added for each abbrev using define-abbrev-table or define-abbrev

If this hook function returns true, and has property no-self-insert true, then the char that triggered the abbrev will not be inserted.

;; attaching a hook when defining an abbrev
(define-abbrev yy-abbrev-table "fn" "function" yy--abhook)

(defun yy--abhook ()
  "Hook function to run after abbrev expansion.
 Mostly used to prevent inserting the char that triggered expansion"
  (message "abbrev hook function ran" )
  t)

;; prevent inserting the char that triggered expansion
(put 'yy--abhook 'no-self-insert t)

Complete Example of Abbrev as 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 yy-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 ((xsyntax-state (syntax-ppss)))
    (not (or (nth 3 xsyntax-state) (nth 4 xsyntax-state)))))

(defun yy-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 (yy-abbrev-enable-function) ; abbrev property :enable-function is ignored, if you define your own expansion function
    (let (
          xp1 xp2
          xabrStr
          xabrSymbol
          )
      (save-excursion
        (forward-symbol -1)
        (setq xp1 (point))
        (forward-symbol 1)
        (setq xp2 (point)))
      (setq xabrStr (buffer-substring-no-properties xp1 xp2))
      (setq xabrSymbol (abbrev-symbol xabrStr))
      (if xabrSymbol
          (progn
            (abbrev-insert xabrSymbol xabrStr xp1 xp2 )
            (yy--abbrev-position-cursor xp1)
            xabrSymbol)
        nil))))

(defun yy--abbrev-position-cursor (&optional Pos)
  "Move cursor back to ▮ if exist.
Pos is a buffer position limit of search backward. If nil, default to point minus 100.
Return point if found, else nil.
Version: 2016-10-24 2023-08-20"
  (interactive)
  (let ((xfoundQ (search-backward "▮" (if Pos Pos (max (point-min) (- (point) 100))) t)))
    (when xfoundQ (delete-char 1))
    xfoundQ
    ))

(defun yy--abhook ()
  "function to run after abbrev expansion
 Mostly used to prevent inserting the char that triggered expansion.
Version 2016-10-24"
  (interactive)
  (message "abbrev hook function ran" )
  t)

(put 'yy--abhook 'no-self-insert t)

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

(define-abbrev-table 'yy-abbrev-table
  '(
    ("fn" "function" yy--abhook)
    ("function" "function ▮name (x) { return x; }" yy--abhook)
    ;; hundreds more
    )
  "Abbrev table for `yy'"
  )

(abbrev-table-put yy-abbrev-table :case-fixed t)
(abbrev-table-put yy-abbrev-table :system t)
(abbrev-table-put yy-abbrev-table :enable-function 'yy-abbrev-enable-function)

(define-derived-mode yy prog-mode "yy"
  "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 'yy-expand-abbrev))
    (progn (add-hook 'abbrev-expand-functions 'yy-expand-abbrev nil t)))

  (abbrev-mode 1)

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

(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.)

Reference

Emacs, Abbrev Mode