Wilfred Hughes::Blog

programming, language design, and human factors

Effortless Major Mode Development

It’s now easier than ever to write major modes in Emacs. If you haven’t written a major mode in a while, or you’re just starting out, here are my three top tips:

Use regexp-opt

As of Emacs 24, regexp-opt takes a 'symbols option. You should write your font-lock keywords like this:

(defvar js-mode-font-lock-keywords
  `((,(regexp-opt
       '("var" "for" "function" "if" "else")
       'symbols)
     . font-lock-keyword-face)

This has two advantages. By whitelisting keywords, users can quickly spot mistakes when editing:

This also prevents a classic bug where Emacs highlights substrings that happen to be keywords:

(Don’t) use company

Company is excellent, and I highly recommend it. However, not all Emacsers use company. You don’t need to force company on your users.

Instead, you can use completion-at-point-functions. Your completion functionality will work in stock Emacs, and company users will benefit too through company-capf.

Ah, but what about all the extra annotations you can supply to company? We can have our cake and eat it too:

(defun racer-complete-at-point ()
  "Complete the symbol at point."
  (unless (nth 3 (syntax-ppss)) ;; not in string
    (let* ((bounds (bounds-of-thing-at-point 'symbol))
           (beg (or (car bounds) (point)))
           (end (or (cdr bounds) (point))))
      (list beg end
            (completion-table-dynamic #'racer-complete)
            :annotation-function #'racer-complete--annotation
            :company-prefix-length (racer-complete--prefix-p beg end)
            :company-docsig #'racer-complete--docsig
            :company-location #'racer-complete--location))))

Test with assess

Historically, it’s been rather awkward to test major modes. Many authors didn’t bother.

That’s all changed with the release of assess. Assess provides great assertions with readable error messages.

For example, here’s a simple indentation test from cask-mode:

(ert-deftest cask-mode-indent-inside-development ()
  "Ensure we correctly indent inside (development ...) blocks."
  (should (assess-indentation=
           'cask-mode
           ;; before:
           "
(development
(depends-on \"foo\"))"
           ;; after:
           "
(development
 (depends-on \"foo\"))")))

Highlighting is particularly helped by assess:

(ert-deftest cask-mode-highlight-sources ()
  "Ensure we highlight known values for source."
  (should (assess-face-at=
           "(source melpa)"
           'cask-mode
           "melpa"
           'cask-mode-source-face)))

If this test fails, we get a helpful message describing which faces were actually used:

#("Face does not match expected value
   Expected: cask-mode-source-face
   Actual: font-lock-keyword-face
   Location: 9
   Line Context: (source melpa)
   bol Position: 1"

These tips are all new things I’ve learnt writing a new major mode for Cask files. If you’re just getting started with Emacs development, check out adding a new language to Emacs. Finally, if I’ve missed your favourite tip, leave a comment on the /r/emacs discussion!