Emacs Lisp: How to Write Keyword Completion Command

By Xah Lee. Date: . Last updated: .

This page shows you how to implement keyword completion in emacs.

xlsl-keyword completion
Keyword completion in emacs.

Problem

You are writing a major mode for your own language. You want keyword completion feature.

When user calls complete-symbol or completion-at-point in your major mode, completion should be done with your language's keywords.

[see Emacs: Name Completion]

What's the difference between complete-symbol and completion-at-point?

complete-symbol

complete-symbol calls either info-complete-symbol or completion-at-point , depending if a prefix arg is given.

If prefix arg is given , it'll complete by using keywords listed in the corresponding Emacs Info Page of the language's index page of manual. (Tip: info manual of a programing language usually do not exist today. in may be common in 90s.)

otherwise, it just call completion-at-point

completion-at-point
do completion at the cursor position.

the actual function called to do the job is in the Hook variable completion-at-point-functions

Solution 1: completion-at-point

Two things you have to do:

The function xx-completion-at-point should take no argument, and return a list of this form:

(START END COLLECTION . PROPS)

Here's the complete code of a major mode with keyword completion feature.

;; sample major mode with keyword completion feature

;; this is your lang's keywords
(setq xx-keywords
      '("touch"
       "touch_start"
       "touch_end"
       "for"
       "foreach"
       "forall"
       ))

(defun xx-completion-at-point ()
  "This is the function to be used for the hook `completion-at-point-functions'."
  (interactive)
  (let* (
         (bds (bounds-of-thing-at-point 'symbol))
         (start (car bds))
         (end (cdr bds)))
    (list start end xx-keywords . nil )))

(define-derived-mode xx-mode c-mode "xx"
  "Major mode for editing xx lang code."
  (add-hook 'completion-at-point-functions 'xx-completion-at-point nil 'local))
  1. Copy and paste the above into a new buffer.
  2. Alt+x eval-buffer.
  3. Open a new buffer.
  4. M-x xx-mode.
  5. Type f, then press Ctrl+Alt+i. Emacs will complete it to become “for”. Press again to see choices.

How does this work?

The command complete-symbol or completion-at-point is emacs standard command to complete word at point. The default key is Ctrl+Alt+i.

complete-symbol is a wrapper to. completion-at-point. You can see in the source code.

The command completion-at-point will just call functions in the variable list completion-at-point-functions.

So, all you have to do is add your own completion function to the list completion-at-point-functions.

Our completion function is xx-completion-at-point, and we added to the hook completion-at-point-functions by this line:

(add-hook 'completion-at-point-functions 'xx-completion-at-point nil 'local)

Solution 2: Using ido for Completion

Here's another way to do completion, using ido mode 's interface.

emacs keyword completion ido
emacs keyword completion with ido

Here is the function that does the completion, using ido interface.

(require 'ido) ; part of emacs

;; this is your lang's keywords
(setq yy-keywords
      '("touch"
       "touch_start"
       "touch_end"
       "for"
       "foreach"
       "forall"
       ))

(defun yy-complete-symbol ()
  "Perform keyword completion on current symbol.
This uses `ido-mode' user interface for completion."
  (interactive)
  (let (xbds xp1 xp2 xcurrent-sym xresult-sym)
    (let ((xbds (bounds-of-thing-at-point 'symbol)))
      (setq xp1 (car xbds)
            xp2 (cdr xbds)))
    (setq xcurrent-sym
          (if (and xp1 xp2 (not (equal xp1 xp2)))
            (buffer-substring-no-properties xp1 xp2)
            ""
            ))
    (setq xresult-sym
          (ido-completing-read "" yy-keywords nil nil xcurrent-sym))
    (delete-region xp1 xp2)
    (insert xresult-sym)))

You'll need to give it a key in your major mode.

[see Emacs Lisp: Create Keymap for Major Mode]

For temp testing, you can do:

(global-set-key (kbd "TAB") 'yy-complete-symbol)

Tip: Emacs 27: ido mode 👎 is becoming obsolete in Emacs 28 (Released 2022-04) , because of new features in [see icomplete] In the above code, you may replace ido-completing-read by completing-read and turn on fido-vertical-mode

Solution 3: Write Your Own Completion Function

Here's alternative way to do completion.

;; this is your lang's keywords
(setq zz-keywords
      '("touch"
       "touch_start"
       "touch_end"
       "for"
       "foreach"
       "forall"
       ))

;; The following is a standalone function that does the completion.

(defun zz-complete-symbol ()
  "Perform keyword completion on word before cursor."
  (interactive)
  (let ((posEnd (point))
        (meat (thing-at-point 'symbol))
        maxMatchResult)

    ;; when nil, set it to empty string, so user can see all lang's keywords.
    ;; if not done, try-completion on nil result lisp error.
    (when (not meat) (setq meat ""))
    (setq maxMatchResult (try-completion meat zz-keywords))

    (cond ((eq maxMatchResult t))
          ((null maxMatchResult)
           (message "Can't find completion for “%s”" meat)
           (ding))
          ((not (string-equal meat maxMatchResult))
           (delete-region (- posEnd (length meat)) posEnd)
           (insert maxMatchResult))
          (t (message "Making completion list…")
             (with-output-to-temp-buffer "*Completions*"
               (display-completion-list
                (all-completions meat zz-keywords)
                meat))
             (message "Making completion list…%s" "done")))))

You'll need to give it a key in your major mode.

[see Emacs Lisp: Create Keymap for Major Mode]

For temp testing, you can just do:

(global-set-key (kbd "TAB") 'zz-complete-symbol)

Then, open a new buffer, type any letter, say “t”, then press Tab, type some more letter, press Tab again.

The above code is very easy to understand. First, you grab the word before cursor, save it as “meat”. Then, you find the maximal match, save it as maxMatchResult. Then, we have a few cases:

  1. If the max match is the same as the word under cursor, then do nothing, because the word is already complete.
  2. If the max match is empty, then tell user there is no completion.
  3. If not the above two cases, then expand the current word to max match.
  4. Otherwise, pop up a dialog to list possible completions.

Lucky for us, emacs does most of the tedious job. The core functions that do the job are:

try-completion
Return the maximal match.
all-completions
Return all possible completions.
display-completion-list
Takes care of the user interface for displaying the possible completions, and making them clickable.

In the above, we used a simple list for our keywords, and fed them to emacs's completion functions. Emacs's completion functions can also take keyword argument in the form of a Association List or Emacs Lisp: Hash Table hashtable.

try-completion and all-completions

Here's detailed comparison of the two most fundamental functions try-completion and all-completions.

try-completion

try-completion
(try-completion STRING COLLECTION &optional PREDICATE)
Return t, nil or a string of longest completion. COLLECTION is a list of strings (it can be list of cons pairs or hashtable and other.).
  • Return t if there's a exact match.
  • Return nil if there is no match.
  • Else, return a string of longest match. That is, in COLLECTION, find all strings that starts with STRING, then, of these, find the longest string that they all share at the start of string.
;; found no match. returns nil
(try-completion
 "c"
 '(
   "amc"
   "amb"
   ))
;; nil

;; only 1 possible match, and is same as input
;; returns t
(try-completion
 "amb"
 '(
   "amc"
   "amb"
   ))
;; t

;; only 1 possible match, and is longer than input
;; returns the matched string
(try-completion
 "ah"
 '(
   "amc"
   "amb"
   "ahu"
   ))
;; "ahu"

;; found more than 1 match. returns the longest start string shared by all matched strings
(try-completion
 "a"
 '(
   "amc"
   "amb"
   ))
;; "am"

all-completions

all-completions
Like try-completion, but it returns a list of all matches.
;; no match
(all-completions
 "c"
 '(
   "amc"
   "amb"
   ))
;; nil

;; only only 1 possible match, same as input
(all-completions
 "amb"
 '(
   "amc"
   "amb"
   ))
;; ("amb")

;; only only 1 possible match, longer than input
(all-completions
 "ah"
 '(
   "amc"
   "amb"
   "ahu"
   ))
;; ("ahu")

;; found more than 1 match. returns them all
(all-completions
 "a"
 '(
   "amc"
   "amb"
   ))
;; ("amc" "amb")

Reference