Emacs Lisp: defvar-keymap hints for which-key

| emacs

Emacs has far too many keyboard shortcuts for me to remember, so I use which-key to show me a menu if I pause for too long and which-key-posframe to put it somewhere close to my cursor.

(use-package which-key :init (which-key-mode 1))
(use-package which-key-posframe :init (which-key-posframe-mode 1))

I've used which-key-replacement-alist to rewrite the function names and re-sort the order to make them a little easier to scan, but that doesn't cover the case where you've defined an anonymous function ((lambda ...)) for those quick one-off commands. It just displays "function".

Pedro A. Aranda Gutiérrez wanted to share this tip about defining hints by using cons. Here's his example:

(defun insert-surround (opening &optional closing)
 "Insert OPENING and CLOSING and place the cursor before CLOSING.

Default CLOSING is \"}\"."
  (insert opening)
  (save-excursion
    (insert (or closing "}"))))

(defvar-keymap tex-format-map
  :doc "My keymap for text formatting"
  "-"  (cons "under" (lambda() (interactive) (insert-surround
"\\underline{")))
  "b"  (cons "bold"  (lambda() (interactive) (insert-surround "\\textbf{")))
  "e"  (cons "emph"  (lambda() (interactive) (insert-surround "\\emph{")))
  "i"  (cons "ital"  (lambda() (interactive) (insert-surround "\\textit{")))
  "m"  (cons "math"  (lambda() (interactive) (insert-surround "$" "$")))
  "s"  (cons "sans"  (lambda() (interactive) (insert-surround "\\textsf{")))
  "t"  (cons "tty" (lambda() (interactive) (insert-surround "\\texttt{")))
  "v"  (cons "Verb"  (lambda() (interactive) (insert-surround "\\Verb{")))
  "S"  (cons "small" (lambda() (interactive) (insert-surround "\\small{"))))
(fset 'tex-format-map tex-format-map)

Let's try it out:

(with-eval-after-load 'tex-mode
  (keymap-set tex-mode-map "C-c t" 'tex-format-map))
2026-03-02_14-45-51.png
Figure 1: Screenshot with hints

This works for named functions as well. Here's how I've updated my config:

(defvar-keymap my-french-map
  "l" (cons "🔍 lookup" #'my-french-lexique-complete-word)
  "w" (cons "📚 wordref" #'my-french-wordreference-lookup)
  "c" (cons "✏️ conj" #'my-french-conjugate)
  "f" (cons "🇫🇷 fr" #'my-french-consult-en-fr)
  "s" (cons "🗨️ say" #'my-french-say-word-at-point)
  "t" (cons "🇬🇧 en" #'my-french-translate-dwim))
(fset 'my-french-map my-french-map)

(with-eval-after-load 'org
  (keymap-set org-mode-map "C-," 'my-french-map)
  (keymap-set org-mode-map "C-c u" 'my-french-map))
2026-03-02_13-57-23.png
Figure 2: Before: Without the cons, which-key uses the full function name
2026-03-02_14-42-42.png
Figure 3: After: Might be easier to skim?

In case you're adding to an existing keymap, you can use keymap-set with cons.

(keymap-set my-french-map "S" (cons "sentence" #'my-french-say-sentence-at-point))

This is also different from the :hints that show up in the minibuffer when you have a repeating map. Those are defined like this:

(defvar-keymap my-french-map
  :repeat (:hints ((my-french-lexique-complete-word . "lookup")
                   (my-french-consult-en-fr . "fr")
                   (my-french-translate-dwim . "en")
                   (my-french-say-word-at-point . "say")))
  "l" (cons "🔍 lookup" #'my-french-lexique-complete-word)
  "w" (cons "📚 wordref" #'my-french-wordreference-lookup)
  "c" (cons "✏️ conj" #'my-french-conjugate)
  "f" (cons "🇫🇷 fr" #'my-french-consult-en-fr)
  "s" (cons "🗨️ say" #'my-french-say-word-at-point)
  "t" (cons "🇬🇧 en" #'my-french-translate-dwim))

and those appear in the minibuffer like this:

2026-03-02_13-59-42.png
Figure 4: Minibuffer repeat hints

Menus in Emacs are also keymaps, but the labels work differently. These ones are defined with easymenu.

(with-eval-after-load 'org
(define-key-after
 org-mode-map
 [menu-bar french-menu]
 (cons "French"
       (easy-menu-create-menu
        "French"
        '(["🕮Grammalecte" my-flycheck-grammalecte-setup t]
          ["✓Gptel" my-lang-gptel-flycheck-setup t]
          ["🎤Subed-record" my-french-prepare-subed-record t])))
 'org))

Using your own hints is like leaving little breadcrumbs for yourself.

Thanks to Pedro for the tip!

View Org source for this post

2026-03-02 Emacs news

| emacs, emacs-news

Hello folks! Last month's Emacs Carnival about completion had 17 posts (nice!), and Philip Kaludercic is hosting this month's Emacs Carnival: Mistakes and Misconceptions. Looking forward to reading your thoughts!

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View Org source for this post

Emacs Carnival Feb 2026 wrap-up: Completion

| emacs

Check out all the wonderful entries people sent in for the Emacs Carnival Feb 2026 theme of Completion:

Also, this one about completing the loop:

Sometimes I miss things, so if you wrote something and you don't see it here, please let me know! Please e-mail me at sacha@sachachua.com or DM me via Mastodon with a link to your post(s). If you like the idea but didn't get something together in time for February, it's never too late. Even if you come across this years later, feel free to write about the topic if it inspires you. I'd love to include a link to your notes in Emacs News.

I added a ton of links from the Emacs News archives to the Resources and Ideas section, so check those out too.

I had a lot of fun learning together with everyone. I already have a couple of ideas for March's Emacs Carnival theme of Mistakes and Misconceptions (thanks to Philip Kaludercic for hosting!), and I can't wait to see what people will come up with next!

View Org source for this post

Using speech recognition for on-the-fly translations in Emacs and faking in-buffer completion for the results

| audio, speech-recognition, emacs, speech

When I'm writing a journal entry in French, I sometimes want to translate a phrase that I can't look up word by word using a dictionary. Instead of switching to a browser, I can use an Emacs function to prompt me for text and either insert or display the translation. The plz library makes HTTP requests slightly neater.

(defun my-french-en-to-fr (text &optional display-only)
  (interactive (list (read-string "Text: ") current-prefix-arg))
  (let* ((url "https://translation.googleapis.com/language/translate/v2")
         (params `(("key" . ,(getenv "GOOGLE_API_KEY"))
                   ("q" . ,text)
                   ("source" . "en")
                   ("target" . "fr")
                   ("format" . "text")))
         (query-string (mapconcat
                        (lambda (pair)
                          (format "%s=%s"
                                  (url-hexify-string (car pair))
                                  (url-hexify-string (cdr pair))))
                        params
                        "&"))
         (full-url (concat url "?" query-string)))
    (let* ((response (plz 'get full-url :as #'json-read))
           (data (alist-get 'data response))
           (translations (alist-get 'translations data))
           (first-translation (car translations))
           (translated-text (alist-get 'translatedText first-translation)))
      (when (called-interactively-p 'any)
        (if display-only
            (message "%s" translated-text)
          (insert translated-text)))
      translated-text)))

I think it would be even nicer if I could use speech synthesis, so I can keep it a little more separate from my typing thoughts. I want to be able to say "Okay, translate …" or "Okay, … in French" to get a translation. I've been using my fork of natrys/whisper.el for speech recognition in English, and I like it a lot. By adding a function to whisper-after-transcription-hook, I can modify the intermediate results before they're inserted into the buffer.

(defun my-whisper-translate ()
  (goto-char (point-min))
  (let ((case-fold-search t))
    (when (re-search-forward "okay[,\\.]? translate[,\\.]? \\(.+\\)\\|okay[,\\.]? \\(.+?\\) in French" nil t)
      (let* ((s (or (match-string 1) (match-string 2)))
             (translation (save-match-data (my-french-en-to-fr s))))
        (replace-match
         (propertize translation
                     'type-hint translation
                     'help-echo s))))))

(with-eval-after-load 'whisper
  (add-hook 'whisper-after-transcription-hook 'my-whisper-translate 70))

But that's too easy. I want to actually type things myself so that I get more practice. Something like an autocomplete suggestion would be handy as a way of showing me a hint at the cursor. The usual completion-at-point functions are too eager to insert things if there's only one candidate, so we'll just fake it with an overlay. This code works only with my whisper.el fork because it supports using a list of functions for whisper-insert-text-at-point.

(defun my-whisper-maybe-type-with-hints (text)
  "Add this function to `whisper-insert-text-at-point'."
  (let ((hint (and text (org-find-text-property-in-string 'type-hint text))))
    (if hint
        (progn
          (my-type-with-hint hint)
          nil)
      text)))

(defvar-local my-practice-overlay nil)
(defvar-local my-practice-target nil)
(defvar-local my-practice-start nil)

(defun my-practice-cleanup ()
  "Remove the overlay and stop monitoring."
  (when (overlayp my-practice-overlay)
    (delete-overlay my-practice-overlay))
  (setq my-practice-overlay nil
        my-practice-target nil
        my-practice-start nil)
  (remove-hook 'post-command-hook #'my-practice-monitor t))

(defun my-practice-monitor ()
  "Updates hint or cancels."
  (let* ((pos (point))
         (input (buffer-substring-no-properties my-practice-start pos))
         (input-len (length input))
         (target-len (length my-practice-target)))
    (cond
     ((or (< pos my-practice-start)
          (> pos (+ my-practice-start target-len))
          (string-match "[\n\t]" input)
          (string= input my-practice-target))
      (my-practice-cleanup))
     ((string-prefix-p (downcase input) (downcase my-practice-target))
      (let ((remaining (substring my-practice-target input-len)))
        (move-overlay my-practice-overlay pos pos)
        (overlay-put my-practice-overlay 'after-string
                     (propertize remaining 'face 'shadow))))
     (t                                 ; typo
      (move-overlay my-practice-overlay pos pos)
      (overlay-put my-practice-overlay 'after-string
                   (propertize (substring my-practice-target input-len) 'face 'error))))))

(defun my-type-with-hint (string)
  "Show hints for STRING."
  (interactive "sString to practice: ")
  (my-practice-cleanup)
  (setq-local my-practice-target string)
  (setq-local my-practice-start (point))
  (setq-local my-practice-overlay (make-overlay (point) (point) nil t t))
  (overlay-put my-practice-overlay 'after-string (propertize string 'face 'shadow))
  (add-hook 'post-command-hook #'my-practice-monitor nil t))

Here's a demonstration of me saying "Okay, this is a test, in French.":

Screencast of using speech recognition to translate into French and provide a hint when typing

Since we're faking in-buffer completion here, maybe we can still get away with considering this as an entry for Emacs Carnival February 2026: Completion ? =)

This is part of my Emacs configuration.
View Org source for this post

Emacs completion and handling accented characters with orderless

| emacs

I like using the orderless completion package for Emacs because it allows me to specify different parts of a completion candidate than any order I want. Because I'm learning French, I want commands like consult-line (which uses minibuffer completion) and completion-at-point (which uses in-buffer completion) to also match candidates where the words might have accented characters. For example, instead of having to type "utilisé" with the accented é, I want to type "utilise" and have it match both "utilise" and "utilisé".

(defvar my-orderless-accent-replacements
  '(("a" . "[aàáâãäå]")
    ("e" . "[eèéêë]")
    ("i" . "[iìíîï]")
    ("o" . "[oòóôõöœ]")
    ("u" . "[uùúûü]")
    ("c" . "[cç]")
    ("n" . "[nñ]"))) ; in case anyone needs ñ for Spanish

(defun my-orderless-accent-dispatch (pattern &rest _)
  (seq-reduce
   (lambda (prev val)
     (replace-regexp-in-string (car val) (cdr val) prev))
   my-orderless-accent-replacements
   pattern))

(use-package orderless
  :custom
  (completion-styles '(orderless basic))
  (completion-category-overrides '((file (styles basic partial-completion))))
  (orderless-style-dispatchers '(my-orderless-accent-dispatch orderless-affix-dispatch)))
2026-02-26_15-06-59.png
Figure 1: Screenshot of consult-line showing matching against accented characters
2026-02-26_15-08-34.png
Figure 2: Screenshot of completion-at-point matching "fev" with "février"

This is an entry for Emacs Carnival February 2026: Completion.

This is part of my Emacs configuration.
View Org source for this post

IndieWeb Carnival February 2026: Intersecting interests

| life
In English

This month, the theme for the IndieWeb Carnival is "Converging Interests." It might actually be easier to list which of my interests don't converge. My interests often overlap. I'll start with a description of my main interests and how they're linked.

Programming is generally useful. I'm particularly interested in automation and cognitive and physical aids like voice interfaces. I love Emacs. It's ostensibly a text editor, but I've tinkered with it to such an extent that I use it for almost everything: managing my notes and tasks, of course, but even recording and editing audio files and organizing my drawings.

Writing helps me think, remember, and share. Org Mode in Emacs allows me to use the technique of literate programming, which combines explaining and coding. Some ideas are easier to think about and express through drawing, which allows me to explore them non-linearly. My drawings apply to all my interests, such as parenting, technology, learning, and planning. Sketchnoting is a great way to learn many things, share my notes, and remember specific moments. For example, my daughter is eager to finish a visual summary we developed together, which was possible because I had written many notes in the web journal I developed and in my French journal.

I've been learning French for the past 4 months, and that also touches various aspects of my daily life. I help my daughter with school, I try to use AI, I tinker with my tools, I watch shows, and I look up words related to my interests. For instance, I updated my handwriting font to include accented letters. This combined drawing, programming, and naturally, learning French. I also modified my writing environment in Emacs to look up words in the dictionary and display AI feedback. I particularly enjoy exploring learning techniques with my daughter, such as flashcards and stories following the principle of comprehensible input. Which methods are effective against which challenges, and how can we make the most of available technology? What we learn will help us across all subjects.

Similarly, learning the piano helps me appreciate the challenge and pleasure of making progress. It's also a good way to help my daughter learn it as well.

Since my life is filled with intertwining interests, it is important to manage my attention despite many distracting temptations, such as programming new tools. I might start a task and then find myself doing something completely different after a series of small, totally logical steps. You know how it goes—one thing leads to another. So I have to write my notes as I go. There is no rush and few of my tasks are urgent, so when I lose my train of thought, I can laugh and look for it again. If I write and share these notes, someone might find them even years later and remind me of them. It is very difficult to choose a moment to stop exploring and to publish my notes. The temptation is always to keep following a new idea.

Fortunately, the cumulative effect of hobbies that complement each other encourages me to grow, and when I am blocked in one direction, one or two other paths usually open up. Speaking of directions, I find it difficult to write when I want to introduce two or more simultaneous streams of ideas because writing is so linear. Still, it's better to write even if it's a bit disjointed.

I think speech recognition helps me capture more ideas, and I'm looking forward to how advances in technology can help me make them happen. I can also get better by learning and linking new curiosities to my other curiosities. I look forward to seeing what kinds of things are possible.

Although I have several hours of freedom now that my daughter can do many things herself, there's always more that I want to learn. Intertwined hobbies thrive, while isolated hobbies are forgotten. For example, I no longer play Stardew Valley since my daughter doesn't play it anymore. It’s a fun game, but if I'm choosing what to spend my time on, I prefer activities that serve multiple goals goals simultaneously. The garden of my interests is not formal and orderly, but rather natural and tangled.

My daughter also has many interests. One year she was interested in Rubik's Cubes and other puzzles; this year she's learning everything about Pokémon. The transience of her interests doesn't bother me. It all combines in unexpected ways. It will be interesting to see how she grows, and to see how I'll grow too.

Thanks to Zachary Kai for hosting the IndieWeb Carnival this month!

En français

Ce mois-ci, le thème du Carnaval IndieWeb est « Intérêts convergents. » C'est peut-être plus facile de lister lesquels de mes centres d'intérêt ne sont pas convergents. Mes centres d'intérêt se recoupent souvent. Je vais commencer par une description de mes premiers intérêts et des façons dont ils sont liés.

La programmation est généralement utile. Je suis particulièrement intéressée par l'automatisation et les aides cognitives et physiques comme l'interface vocale. J'adore Emacs, qui est un éditeur de texte, mais je le bricole à tel point que je l'utilise pour presque tout : gérer mes notes et mes tâches, bien sûr, mais même enregistrer et éditer des fichiers audio et organiser mes dessins.

L'écriture m'aide à penser, à me remémorer et à partager. Org Mode sous Emacs me permet d'utiliser la technique de « programmation lettrée », qui est la combinaison de l'explication et de la programmation. Quelques idées sont plus faciles à penser et à exprimer par le dessin, lequel me permet de les explorer non linéairement. Mes dessins s'appliquent aussi à tous mes centres d'intérêt, comme la parentalité, la technologie, l'apprentissage et la planification. Le sketchnoting est une bonne manière d'apprendre beaucoup de choses, de partager mes notes et de me souvenir de certains moments. Par exemple, ma fille a hâte de finir une synthèse visuelle que nous avons élaborée ensemble, et qui est possible parce que j'avais écrit beaucoup de notes dans le journal web que j'avais développé et dans mon journal en français.

L'apprentissage du français depuis 4 mois touche aussi divers aspects de ma vie quotidienne. J'aide ma fille à l'école, j'essaie d'utiliser l'IA, je bricole mes outils, je regarde des émissions, je cherche des mots pour mes centres d'intérêt. Par exemple, j'ai mis à jour la police de caractères de mon écriture pour inclure les lettres accentuées. Cela a associé le dessin, la programmation, et naturellement l'apprentissage du français. J'ai aussi modifié mon environnement d'écriture sous Emacs pour rechercher les mots dans le dictionnaire et pour afficher les commentaires de l'IA. J'aime particulièrement explorer des techniques d'apprentissage avec ma fille comme les cartes mémoire et les histoires qui suivent le principe de l'apport compréhensible. Quelles méthodes sont efficaces contre quels défis, et comment nous pouvons tirer le meilleur parti des technologies disponibles ? Ce que nous apprenons nous servira bien dans tous les sujets.

De la même manière, l'apprentissage du piano m'aide à apprécier le défi et le plaisir de progresser. Une autre raison de le faire est qu'il aide ma fille à l'apprendre aussi.

Comme ma vie est remplie d'intérêts qui s'entrelacent, c'est important de gérer mon attention face à plusieurs tentations de s'éparpiller, comme la programmation de nouvelles automatisations. Je commence peut-être une tâche et je me retrouve ensuite à faire une tâche complètement différente après une suite d'étapes logiques. On sait ce que c'est, de fil en aiguille. Donc je dois écrire mes notes au fur et à mesure. Rien ne me presse et peu de mes tâches sont urgentes, donc quand je perds le fil de mes pensées, je peux rire et le retrouver. Si j'écris et que je partage ces notes, quelqu'un peut les trouver même après plusieurs années et me les rappeler. C'est très difficile de choisir un moment où j'arrête d'explorer et où je publie mes notes. La tentation est toujours de continuer à suivre une nouvelle idée.

Heureusement, l'effet cumulatif de loisirs qui se complètent m'encourage à grandir, et quand je suis bloquée dans une direction, une ou deux autres pistes se sont ouvertes. En parlant de directions, je trouve que c'est difficile d'écrire quand je veux introduire deux ou plusieurs suites d'idées simultanées, à cause de la linéarité de l'écriture. De toute façon, c'est mieux d'écrire même si c'est un peu décousu.

Je pense que la reconnaissance vocale m'aide à saisir plus d'idées et les progrès technologiques m'aident à les exécuter. Je vais aussi m'améliorer en apprenant et en reliant de nouvelles curiosités à mes autres curiosités. J'ai hâte de voir quelles sortes de choses sont possibles.

Bien que j'aie plusieurs heures de liberté maintenant que ma fille est capable de faire beaucoup de choses elle-même, il y a toujours plus de choses que je veux apprendre. Les loisirs entrelacés se développent, tandis que les loisirs isolés sont oubliés. Par exemple, je ne joue plus à Stardew Valley maintenant que ma fille n'y joue plus. C'est un jeu amusant, mais si je peux choisir un passe-temps, j'en préfère un qui serve des objectifs multiples simultanés. Le jardin de mes intérêts n'est pas formel et ordonné, mais plutôt naturel et entremêlé.

Ma fille a aussi beaucoup de centres d'intérêt. Une année elle s'est intéressée au Cube de Rubik et aux autres casse-têtes, une autre année elle apprenait tout sur Pokémon. Ça ne me dérange pas, tout se combine de façons inattendues. Ce sera intéressant de voir comment elle grandira, et moi aussi.

Merci à Zachary Kai d'accueillir le Carnaval IndieWeb ce mois-ci !

View Org source for this post

Sorting completion candidates, such as sorting Org headings by level

| emacs, org

: Made the code even neater with :key, included the old code as well

At this week's Emacs Berlin meetup, someone wanted to know how to change the order of completion candidates. Specifically, they wanted to list the top level Org Mode headings before the second level headings and so on. They were using org-ql to navigate Org headings, but since org-ql sorts its candidates by the number of matches according to the code in the org-ql-completing-read function, I wasn't quite sure how to get it to do what they wanted. (And I realized my org-ql setup was broken, so I couldn't fiddle with it live. Edit: Turns out I needed to update the peg package) Instead, I showed folks consult-org-heading which is part of the Consult package, which I like to use to jump around the headings in a single Org file. It's a short function that's easy to use as a starting point for something custom.

Here's some code that allows you to use consult-org-heading to jump to an Org heading in the current file with completions sorted by level.

(with-eval-after-load 'consult-org
  (advice-add
   #'consult-org--headings
   :filter-return
   (lambda (candidates)
     (sort candidates
           :key (lambda (o) (car (get-text-property 0 'consult-org--heading o)))))))
2026-02-26_13-42-58.png
Figure 1: Screenshot showing where the candidates transition from top-level headings to second-level headings

My previous approach defined a different function based on consult-org-heading, but using the advice feels a little cleaner because it will also make it work for any other function that uses consult-org--headings. I've included the old code in case you're curious. Here, we don't modify the function's behaviour using advice, we just make a new function (my-consult-org-heading) that calls another function that processes the results a little (my-consult-org--headings).

Old code, if you're curious
(defun my-consult-org--headings (prefix match scope &rest skip)
  (let ((candidates (consult-org--headings prefix match scope)))
    (sort candidates
          :lessp
          (lambda (a b)
            (let ((level-a (car (get-text-property 0 'consult-org--heading a)))
                  (level-b (car (get-text-property 0 'consult-org--heading b))))
              (cond
               ((< level-a level-b) t)
               ((< level-b level-a) nil)
               ((string< a b) t)
               ((string< b a) nil)))))))

(defun my-consult-org-heading (&optional match scope)
  "Jump to an Org heading.

MATCH and SCOPE are as in `org-map-entries' and determine which
entries are offered.  By default, all entries of the current
buffer are offered."
  (interactive (unless (derived-mode-p #'org-mode)
                 (user-error "Must be called from an Org buffer")))
  (let ((prefix (not (memq scope '(nil tree region region-start-level file)))))
    (consult--read
     (consult--slow-operation "Collecting headings..."
       (or (my-consult-org--headings prefix match scope)
           (user-error "No headings")))
     :prompt "Go to heading: "
     :category 'org-heading
     :sort nil
     :require-match t
     :history '(:input consult-org--history)
     :narrow (consult-org--narrow)
     :state (consult--jump-state)
     :annotate #'consult-org--annotate
     :group (and prefix #'consult-org--group)
     :lookup (apply-partially #'consult--lookup-prop 'org-marker))))

I also wanted to get this to work for C-u org-refile, which uses org-refile-get-location. This is a little trickier because the table of completion candidates is a list of cons cells that don't store the level, and it doesn't pass the metadata to completing-read to tell it not to re-sort the results. We'll just fake it by counting the number of "/", which is the path separator used if org-outline-path-complete-in-steps is set to nil.

(with-eval-after-load 'org
  (advice-add
   'org-refile-get-location
   :around
   (lambda (fn &rest args)
     (let ((completion-extra-properties
            '(:display-sort-function
              (lambda (candidates)
                (sort candidates
                      :key (lambda (s) (length (split-string s "/"))))))))
       (apply fn args)))))
2026-02-26_14-01-28.png
Figure 2: Screenshot of sorted refile entries

In general, if you would like completion candidates to be in a certain order, you can specify display-sort-function either by calling completing-read with a collection that's a lambda function instead of a table of completion candidates, or by overriding it with completion-category-overrides if there's a category you can use or completion-extra-properties if not.

Here's a short example of passing a lambda to a completion function (thanks to Manuel Uberti):

(defun mu-date-at-point (date)
  "Insert current DATE at point via `completing-read'."
  (interactive
   (let* ((formats '("%Y%m%d" "%F" "%Y%m%d%H%M" "%Y-%m-%dT%T"))
          (vals (mapcar #'format-time-string formats))
          (opts
           (lambda (string pred action)
             (if (eq action 'metadata)
                 '(metadata (display-sort-function . identity))
               (complete-with-action action vals string pred)))))
     (list (completing-read "Insert date: " opts nil t))))
  (insert date))

If you use consult--read from the Consult completion framework, there is a :sort property that you can set to either nil or your own function.

This entry is part of the Emacs Carnival for Feb 2026: Completion.

This is part of my Emacs configuration.
View Org source for this post