Skip to content

Tips for Creating Commands

Daniel Mendler edited this page Feb 17, 2021 · 8 revisions

Table of Contents

Associating Data with a Candidate

Sometimes, one wishes to select a candidate from a list, but actually use data associated with the candidate instead of the candidate itself.

For example, consider the Swiper-like command in which a user searches for a matching line. Although users choose a candidate line based on the candidate's text, the command actually needs the candidate's line number, not the text of said line, in order to jump to the right place.

There are potentially many ways to associate data with a candidate, but here are a few:

  1. Add text properties to the candidate.

    One way to use this approach is to add properties using the function propertize, and then retrieve that property from the selected candidate using get-text-property. One shortcoming to this approach is that completing-read tends to remove text properties when completing. selectrum--read does not have that limitation, but by using it, your command is no longer compatible with other completion frameworks, such as Icomplete.

    This situation might improve in the future in Emacs 28, using the new minibuffer-allow-text-properties.

    ;; Add the property, such as with `mapcar'.
    (propertize "some candidate"
                'my-property my-data)
    ;; ... Later, retrieve the property.
    (get-text-property 0 'my-property selected-candidate)
  2. Use an alist of candidate-data pairs.

    An alist is a list of key-value pairs. When passed an alist, completing-read will automatically use the first item in the list as the candidate. Once a candidate is selected, you can get its associated data from the alist using the functions assoc (which gets the first pair for a given key) and cdr (to get the value of the association).

    When comparing string keys, remember to use one of

    • equal, which assoc uses by default
    • string-equal, which is equivalent to equal, but raises an error when arguments aren't strings or symbols
    • equal-including-properties, which also considers text properties, unlike equal and string-equal.

    When using this approach, keep in mind that your candidates' contents must be unique, as text properties tend to be stripped and assoc will only return the first matching key-value pair.

    (let* ((my-pairs ...)
           (chosen-candidate "Choose: " my-pairs)
           (associated-data (cdr (assoc chosen-candidate my-pairs))))
      ...)
  3. Format the candidate to include the extra information.

    In the case of jumping to matching lines, this might mean prepending the line number to the front of the candidate, such as in ("Line 1: This is my first line of text." "Line 2: This is the second line."). The data could then be extracted using the function substring and parsing functions like string-to-number.

    Although simple, this approach does have its downsides. For example, in the Swiper-like command, if each candidate includes a line number, then it becomes harder to search for numbers that are actually in the buffer. Since any part of the candidate can be matched against, this can result in many false positives.

Completion Metadata and Related Information

Completion meta-data provides Emacs with extra information about the candidates, such as their annotations or how they should be sorted.

For some features, Selectrum has its own internal way of expressing the same information that is described by completion metadata, and these ways are often simpler, but using completion metadata should work with all completion interfaces (such as Helm, Ivy, Icomplete, and the default completion UI). When writing your commands to use metadata instead of package-specific features, all Emacs users can benefit from your work.

The function complete-with-action can be used to simplify writing your own completion tables (see examples below).

See the Emacs Lisp reference manual on Programmed Completion for more information.

Annotating Candidates

An annotation is text displayed next to a candidate. An annotation is not part of its respective candidate, and does not affect the return value of completion functions like completing-read. For example, Selectrum shows a function's documentation string when completing function names with completion-at-point. You can see this by doing the following:

  1. Call eval-expression (M-:) so that you can type a Lisp expression.
  2. Type (complet. This makes it unambiguous that we are typing a function name.
  3. Call completion-at-point (M-TAB or C-M-i) to complete the function name.
  4. You will see that Selectrum annotates the candidates by displaying their respective documentation string at the right margin.

In Emacs's default completion UI, annotations are shown next to the candidate in the *Completetions* buffer. In Selectrum, they are shown to the right of the candidate in the minibuffer (or in the case of commands that use completion-in-region, at the right margin) on the same line as the candidate.

When using Selectrum-specific features, we have 2 main options:

  • To tell Selectrum to display the annotation just after the candidate, propertize your candidate with the text property selectrum-candidate-display-suffix.
  • To tell Selectrum to display some text at the right margin, propertize your candidate with the text property selectrum-candidate-display-right-margin.

See Selectrum's README.md for more information about these and other text properties that Selectrum uses.

(completing-read
 "Display some text to the right of candidate: "
 (list (propertize
        "my candidate"
        'selectrum-candidate-display-suffix
        (propertize " - My candidate suffix."
                    'face 'completions-annotations))))
(completing-read
 "Display some text at right margin: "
 (list (propertize
        "my candidate"
        'selectrum-candidate-display-right-margin
        (propertize "Text at right margin"
                    'face 'completions-annotations))))

Instead of relying on Selectrum-specific features, one could also use the annotation-function property of completion metadata. This property should be a function that takes a string candidate and returns a string to display after the candidate. There are two main consequences of this approach:

  1. Annotations can be created dynamically.
  2. It takes multiple function calls, as opposed to the ability to generate the annotations at the same time as the candidates in the Selectrum-specific method.
(completing-read
 "Use annotations to note which is longest: "
 (lambda (input predicate action)
   (if (eq action 'metadata)
       `(metadata
         (annotation-function
          . (lambda (str)
              (when (string= str "longest")
                " <- This is the longest."))))
     (complete-with-action action
                           '("longest" "short" "longer")
                           input
                           predicate))))

Sorting Candidates

Generally, candidates are sorted using the function found in the variable selectrum-preprocess-candidates-function and according the value of selectrum-should-sort. This is not the same as moving the default candidate to the top of the list, which is determined by the no-move-default-candidate parameter of selectrum--read.

In a custom Selectrum command, you can disable the sorting of candidates (independent of whether the default candidate will be moved) by wrapping your completing code in a let expression and setting selectrum-should-sort to nil. By default, Selectrum sorts candidates by length, displaying shorter candidates at the top of the list.

(let ((selectrum-should-sort nil))
  (completing-read "Not sorted by length: "
                   '("longest" "short" "longer")))

A more general method of controlling sorting (which should work everywhere) is to set the display-sort-function property in your candidates' completion metadata. This property is a function that takes a list of candidates, and returns a sorted list. Therefore, one can use the function identity to disable sorting, because it will return the list it receives.

(completing-read
 "Not sorted: "
 (lambda (input predicate action)
   (if (eq action 'metadata)
       `(metadata
         (display-sort-function . identity))
     (complete-with-action action
                           '("longest" "short" "longer")
                           input
                           predicate))))