-
Notifications
You must be signed in to change notification settings - Fork 33
Tips for Creating Commands
Table of Contents
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:
-
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 usingget-text-property
. One shortcoming to this approach is thatcompleting-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)
-
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 functionsassoc
(which gets the first pair for a given key) andcdr
(to get the value of the association).When comparing string keys, remember to use one of
-
equal
, whichassoc
uses by default -
string-equal
, which is equivalent toequal
, but raises an error when arguments aren't strings or symbols -
equal-including-properties
, which also considers text properties, unlikeequal
andstring-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)))) ...)
-
-
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 functionsubstring
and parsing functions likestring-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 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.
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:
- Call
eval-expression
(M-:
) so that you can type a Lisp expression. - Type
(complet
. This makes it unambiguous that we are typing a function name. - Call
completion-at-point
(M-TAB
orC-M-i
) to complete the function name. - 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:
- Annotations can be created dynamically.
- 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))))
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))))