-
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-readtends to remove text properties when completing.selectrum-readdoes 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
alistof candidate-data pairs.An alist is a list of key-value pairs. When passed an alist,
completing-readwill 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, whichassocuses 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, unlikeequalandstring-equal.
When using this approach, keep in mind that your candidates' contents must be unique, as text properties tend to be stripped and
assocwill 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 functionsubstringand 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.
Selectrum sometimes has its own internal way of expressing the same ideas as
completion metadata, and these ways are often simpler, but using completion
metadata should work with all other completion interfaces (such as
Helm,
Ivy,
Icomplete,
and the default completion
UI). When
creating a completion table with metadata, the function complete-with-action
can be very useful.
See the Emacs Lisp reference manual on Programmed Completion for more information.
An annotation is extra information related to a candidate, such as what you
might see when completing Elisp functions names with the command
completion-at-point. They are not part of the candidate itself, only displayed
next to it.
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 (in the case of
completion-in-region, at the right margin) on the same line.
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 the other text properties 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" "longer" "short")
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-p. 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-p to
nil.
(let ((selectrum-should-sort-p nil))
(completing-read "Not sorted by length: "
'("longest" "longer" "short")))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" "longer" "short")
input
predicate))))