Skip to content
John Hamelink edited this page Jun 11, 2024 · 227 revisions

Consult is documented in the README, see in particular the configuration section. On this wiki page auxiliary configuration and small utility commands are documented. Feel free to contribute your own useful configuration snippets, candidate multi sources or Consult-related commands!

The snippets on this page REQUIRE lexical binding. Lexical binding is usually specified as a file-local variable. To activate lexical binding, add ;; -*- lexical-binding: t -*- to the top of your source file. If you use a literate org file, adding this block to the very top of it should do the trick:

#+begin_src emacs-lisp :comments no :tangle yes
  ;; -*- lexical-binding: t -*-
#+end_src

Configuration

Manual preview for non-Consult commands using Embark

(define-key minibuffer-local-map (kbd "M-.") #'my-embark-preview)
(defun my-embark-preview ()
  "Previews candidate in vertico buffer, unless it's a consult command"
  (interactive)
  (unless (bound-and-true-p consult--preview-function)
    (save-selected-window
      (let ((embark-quit-after-action nil))
        (embark-dwim)))))

Toggle preview during active completion session

It is possible to enable/disable preview during an active completing-read session, by writing a small command. See #233.

(defvar-local consult-toggle-preview-orig nil)

(defun consult-toggle-preview ()
  "Command to enable/disable preview."
  (interactive)
  (if consult-toggle-preview-orig
      (setq consult--preview-function consult-toggle-preview-orig
            consult-toggle-preview-orig nil)
    (setq consult-toggle-preview-orig consult--preview-function
          consult--preview-function #'ignore)))

(define-key vertico-map (kbd "M-P") #'consult-toggle-preview)

Configure S-up/S-down preview keys

It is possible to configure multiple preview keys, for example S-up/S-down, such that one can scroll over the list of candidates while doing preview. For Vertico the following configuration can be used.

(define-key vertico-map [S-up] #'vertico-previous)
(define-key vertico-map [S-down] #'vertico-next)
(consult-customize consult-recent-file :preview-key '([S-up] [S-down]))

Add command-local keybinding

For example if consult-line is bound to C-s, you may want to load the latest search term when pressing C-s C-s. This can be achieved by binding C-s in the consult-line local keymap.

(defvar my-consult-line-map
  (let ((map (make-sparse-keymap)))
    (define-key map "\C-s" #'previous-history-element)
    map))

(consult-customize consult-line :keymap my-consult-line-map)

Add category-specific minibuffer keybindings

As a less fine-grained alternative to the above method, the following function creates minibuffer keybindings that take effect only for specific categories of items.

(defun define-minibuffer-key (key &rest defs)
  "Define KEY conditionally in the minibuffer.
DEFS is a plist associating completion categories to commands."
  (define-key minibuffer-local-map key
    (list 'menu-item nil defs :filter
          (lambda (d)
            (plist-get d (completion-metadata-get
                          (completion-metadata (minibuffer-contents)
                                               minibuffer-completion-table
                                               minibuffer-completion-predicate)
                          'category))))))

For instance, the following binds C-s to previous-history-element not only in consult-line, but also in consult-oultine, consult-mark, etc; moreover, it binds C-s to consult-find-for-minibuffer (defined below in this wiki) whenever the minibuffer is reading a file name.

(define-minibuffer-key "\C-s"
  'consult-location #'previous-history-element
  'file #'consult-find-for-minibuffer)

consult-outline support for eshell prompts

In order to support quick jumping to prompts in eshell via consult-outline we can set the outline-regexp appropriately in the eshell-mode (#130).

(add-hook 'eshell-mode-hook (lambda () (setq outline-regexp eshell-prompt-regexp)))

Hide all sources, except normal buffers in consult-buffer by default

Makes only the open buffers list visible when calling consult-buffer command by hiding the other sources, but still allowing the narrowing to recent files (by typing f SPC), bookmarks (m SPC) and project buffer and/or files (p SPC).

See issue #203 for more context and use case example.

(dolist (src consult-buffer-sources)
  (unless (eq src 'consult--source-buffer)
    (set src (plist-put (symbol-value src) :hidden t))))

Start command with initial narrowing

Start with initial narrowing (#203). Note that there is also the possibility to mark sources as initially :hidden. This is an alternative to initial narrowing.

;; Configure initial narrowing per command
(defvar consult-initial-narrow-config
  '((consult-buffer . ?b)))

;; Add initial narrowing hook
(defun consult-initial-narrow ()
  (when-let (key (alist-get this-command consult-initial-narrow-config))
    (setq unread-command-events (append unread-command-events (list key 32)))))
(add-hook 'minibuffer-setup-hook #'consult-initial-narrow)

Cycle through narrowing keys

You may want to cycle through all the narrowing keys with convenient left and right key bindings. See issue #337 for more context. Note that Vertico provides the commands vertico-next-group and vertico-previous-group which allows to cycle through the groups. The Vertico group cycling is an alternative to the commands described here.

(define-key consult-narrow-map [C-left] #'consult-narrow-cycle-backward)
(define-key consult-narrow-map [C-right] #'consult-narrow-cycle-forward)

(defun consult-narrow-cycle-backward ()
  "Cycle backward through the narrowing keys."
  (interactive)
  (when consult--narrow-keys
    (consult-narrow
     (if consult--narrow
         (let ((idx (seq-position consult--narrow-keys
                                  (assq consult--narrow consult--narrow-keys))))
           (unless (eq idx 0)
             (car (nth (1- idx) consult--narrow-keys))))
       (caar (last consult--narrow-keys))))))

(defun consult-narrow-cycle-forward ()
  "Cycle forward through the narrowing keys."
  (interactive)
  (when consult--narrow-keys
    (consult-narrow
     (if consult--narrow
         (let ((idx (seq-position consult--narrow-keys
                                  (assq consult--narrow consult--narrow-keys))))
           (unless (eq idx (1- (length consult--narrow-keys)))
             (car (nth (1+ idx) consult--narrow-keys))))
       (caar consult--narrow-keys)))))

Previewing files in find-file

We can enable preview in find-file by providing a custom read-file-name-function. A similar approach could work for read-buffer-function such that all commands reading a buffer name would preview the buffer.

(setq read-file-name-function #'consult-find-file-with-preview)

(defun consult-find-file-with-preview (prompt &optional dir default mustmatch initial pred)
  (interactive)
  (let ((default-directory (or dir default-directory))
        (minibuffer-completing-file-name t))
    (consult--read #'read-file-name-internal :state (consult--file-preview)
                   :prompt prompt
                   :initial initial
                   :require-match mustmatch
                   :predicate pred)))

Orderless style dispatchers (Ensure that the $ regexp works with consult-buffer)

Unfortunately $ does not work out of the box with consult-buffer and consult-line since these commands add disambiguation suffixes to the candidate strings. The problem can be fixed by adjusting the filter regular expressions accordingly. See this reddit post for more context.

(defun fix-dollar (args)
  (if (string-suffix-p "$" (car args))
      (list (format "%s[%c-%c]*$"
                    (substring (car args) 0 -1)
                    consult--tofu-char
                    (+ consult--tofu-char consult--tofu-range -1)))
    args))
(advice-add #'orderless-regexp :filter-args #'fix-dollar)
(advice-add #'prescient-regexp-regexp :filter-args #'fix-dollar)

I recommend to use the Orderless style dispatchers for a more robust solution. See the next section for a sophisticated Orderless configuration.

@minad’s orderless configuration

(use-package orderless
  :demand t
  :config

  (defun +orderless--consult-suffix ()
    "Regexp which matches the end of string with Consult tofu support."
    (if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
        (format "[%c-%c]*$"
                consult--tofu-char
                (+ consult--tofu-char consult--tofu-range -1))
      "$"))

  ;; Recognizes the following patterns:
  ;; * .ext (file extension)
  ;; * regexp$ (regexp matching at end)
  (defun +orderless-consult-dispatch (word _index _total)
    (cond
     ;; Ensure that $ works with Consult commands, which add disambiguation suffixes
     ((string-suffix-p "$" word)
      `(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--consult-suffix))))
     ;; File extensions
     ((and (or minibuffer-completing-file-name
               (derived-mode-p 'eshell-mode))
           (string-match-p "\\`\\.." word))
      `(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--consult-suffix))))))

  ;; Define orderless style with initialism by default
  (orderless-define-completion-style +orderless-with-initialism
    (orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp)))

  ;; You may want to combine the `orderless` style with `substring` and/or `basic`.
  ;; There are many details to consider, but the following configurations all work well.
  ;; Personally I (@minad) use option 3 currently. Also note that you may want to configure
  ;; special styles for special completion categories, e.g., partial-completion for files.
  ;;
  ;; 1. (setq completion-styles '(orderless))
  ;; This configuration results in a very coherent completion experience,
  ;; since orderless is used always and exclusively. But it may not work
  ;; in all scenarios. Prefix expansion with TAB is not possible.
  ;;
  ;; 2. (setq completion-styles '(substring orderless))
  ;; By trying substring before orderless, TAB expansion is possible.
  ;; The downside is that you can observe the switch from substring to orderless
  ;; during completion, less coherent.
  ;;
  ;; 3. (setq completion-styles '(orderless basic))
  ;; Certain dynamic completion tables (completion-table-dynamic)
  ;; do not work properly with orderless. One can add basic as a fallback.
  ;; Basic will only be used when orderless fails, which happens only for
  ;; these special tables.
  ;;
  ;; 4. (setq completion-styles '(substring orderless basic))
  ;; Combine substring, orderless and basic.
  ;;
  (setq completion-styles '(orderless basic)
        completion-category-defaults nil
        ;;; Enable partial-completion for files.
        ;;; Either give orderless precedence or partial-completion.
        ;;; Note that completion-category-overrides is not really an override,
        ;;; but rather prepended to the default completion-styles.
        ;; completion-category-overrides '((file (styles orderless partial-completion))) ;; orderless is tried first
        completion-category-overrides '((file (styles partial-completion)) ;; partial-completion is tried first
                                        ;; enable initialism by default for symbols
                                        (command (styles +orderless-with-initialism))
                                        (variable (styles +orderless-with-initialism))
                                        (symbol (styles +orderless-with-initialism)))
        orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
        orderless-style-dispatchers (list #'+orderless-consult-dispatch
                                          #'orderless-affix-dispatch)))

Use Orderless as pattern compiler for consult-grep/ripgrep/find

consult-ripgrep and the other commands use Emacs regular expressions by default, which are translated to the PCRE/ERE regular expression syntax. It is possible to plug-in Orderless as pattern compiler. See issue #380 and #381 for more information.

(defun consult--orderless-regexp-compiler (input type &rest _config)
  (setq input (orderless-pattern-compiler input))
  (cons
   (mapcar (lambda (r) (consult--convert-regexp r type)) input)
   (lambda (str) (orderless--highlight input t str))))

;; OPTION 1: Activate globally for all consult-grep/ripgrep/find/...
;; (setq consult--regexp-compiler #'consult--orderless-regexp-compiler)

;; OPTION 2: Activate only for some commands, e.g., consult-ripgrep!
(defun consult--with-orderless (&rest args)
  (minibuffer-with-setup-hook
      (lambda ()
        (setq-local consult--regexp-compiler #'consult--orderless-regexp-compiler))
    (apply args)))
(advice-add #'consult-ripgrep :around #'consult--with-orderless)

Skipping directories when using consult-find

If you find consult-find slow and would like to skip some directories, consider specifying the directories to skip using consult-find-args:

(setq consult-find-args "find . -not ( -wholename */.* -prune -o -name node_modules -prune )")

Default consult-find-args:

(setq consult-find-args "find . -not ( -wholename */.* -prune )")

Use consult-ripgrep instead of project-find-regexp in project.el

By default, project-find-regexp uses grep and also it does not offer the convenient and beloved interface of Consult. You might want to use consult-ripgrep in place of it, when using project.el.

(require 'keymap) ;; keymap-substitute requires emacs version 29.1?
(require 'cl-seq)

(keymap-substitute project-prefix-map #'project-find-regexp #'consult-ripgrep)
(cl-nsubstitute-if
  '(consult-ripgrep "Find regexp")
  (pcase-lambda (`(,cmd _)) (eq cmd #'project-find-regexp))
  project-switch-commands)

Unstable hacks and advices

Make consult-imenu ignore group titles when searching with orderless

consult-imenu moves the top-most imenu names to the group title via the group-function and adds narrowing if configured in consult-imenu-config. This grouping only makes sense for certain major modes, e.g., elisp where the topmost menu name corresponds to fixed imenu item categories, e.g., “Functions”, “Variables”, etc. In contrast, for org-mode using the top menu names as group titles does not make sense, since they depend on the buffer content.

The top most menu name is only moved visually to the group title, but the title is still included with the candidate text, and remains searchable. This means that searching for, e.g., ion in an emacs-lisp buffer will by default match all functions and not just function names containing those letters.

If you don’t want the group titles for modes configured in consult-imenu-config to be searchable, it is possible to advise orderless, such that it ignores the top most menu names/group titles when searching candidates. The following implements this, while also allowing embark collect to visit a single imenu entry from the collect buffer.

(defun my/consult-imenu-around-advice (ci-orig &rest r)
  "Patch orderless to inhibit matching group categories in consult-imenu."
  (if-let* ((config (cdr (seq-find (lambda (x) (derived-mode-p (car x)))
                                   consult-imenu-config)))
            (types (plist-get config :types))
            (types-regex (rx-to-string
                          `(and line-start (or ,@(mapcar #'cadr types)) ? ))))
      (cl-letf* ((of-orig (symbol-function 'orderless-filter))
                 ((symbol-function 'orderless-filter) ;patch pattern compiler within filter
                  (lambda (&rest r)
                    (cl-letf* ((opc-orig (symbol-function 'orderless-pattern-compiler))
                               ((symbol-function 'orderless-pattern-compiler)
                                (lambda (&rest r)
                                  (if (and (eq (length r) 1) ;single match string starts
                                           (string-match-p types-regex (car r)))
                                      (apply opc-orig r)
                                    (mapcar (lambda (x) ;replace beginning-of-string
                                              (if (string-match (regexp-quote "\\`" ) x)
                                                  (concat types-regex
                                                          (replace-match "\\b" nil t x))
                                                (concat types-regex ".*?" x)))
                                            (apply opc-orig r))))))
                      (apply of-orig r))))
                 (oh-orig (symbol-function 'orderless--highlight))
                 ((symbol-function 'orderless--highlight) ; patch highlighter to skip type
                  (lambda (regexps string)
                    (if-let* ((pref
                               (next-single-property-change 0 'consult--type string)))
                        (cl-letf* ((sm-orig (symbol-function 'string-match))
                                   ((symbol-function 'string-match)
                                    (lambda (re str)
                                      (funcall sm-orig re str (1+ pref)))))
                          (funcall oh-orig regexps string))
                      (funcall oh-orig regexps string)))))
        (apply ci-orig r))
    (apply ci-orig r)))

(advice-add #'consult-imenu :around #'my/consult-imenu-around-advice))

Pre-select nearest heading for consult-org-heading and consult-outline using vertico

When looking at an outline of the current buffer, it can be nice to see the current and surrounding headings for context and to jump to nearby headings. These pieces of code will make the nearest heading or imenu item be automatically selected when those minibuffer commands are run.

First, using advice, save the current location in the buffer.

(defvar consult--previous-point nil
    "Location of point before entering minibuffer.
Used to preselect nearest headings and imenu items.")

(defun consult--set-previous-point (&optional arg1 arg2)
  "Save location of point. Used before entering the minibuffer."
  (setq consult--previous-point (point)))

(advice-add #'consult-org-heading :before #'consult--set-previous-point)
(advice-add #'consult-outline :before #'consult--set-previous-point)

Advise vertico--update to select the nearest candidate if applicable.

(advice-add #'vertico--update :after #'consult-vertico--update-choose)

(defun consult-vertico--update-choose (&rest _)
  "Pick the nearest candidate rather than the first after updating candidates."
  (when (and consult--previous-point
             (memq current-minibuffer-command
                   '(consult-org-heading consult-outline)))
    (setq vertico--index
          (max 0 ; if none above, choose the first below
               (1- (or (seq-position
                        vertico--candidates
                        consult--previous-point
                        (lambda (cand point-pos) ; counts on candidate list being sorted
                          (> (cl-case current-minibuffer-command
                               (consult-outline
                                (car (consult--get-location cand)))
                               (consult-org-heading
                                (get-text-property 0 'consult--candidate cand)))
                             point-pos)))
                       (length vertico--candidates))))))
  (setq consult--previous-point nil))

Temporarily override consult-ripgrep-args

I am really not sure about the reason, maybe because the builder is called asynchronously, but to temporarily override consult-ripgrep-args, you may need to wrap the entire consult--ripgrep-builder. Here is an example that temporarily adds --no-ignore-vcs flag to the builder. Using advice-add and advice-remove to override consult--ripgrep-builder seems also OK, but I haven’t tried it.

(defun consult--ripgrep-noignore-builder (input)
  "consult--ripgrep-builder with INPUT, but ignores .gitignore."
  (let ((consult-ripgrep-args
         (if (string-match-p "--no-ignore-vcs" consult-ripgrep-args)
             consult-ripgrep-args
           (concat consult-ripgrep-args "--no-ignore-vcs ."))))
    (consult--make-ripgrep-builder input)))

(defun consult-ripgrep-noignore (&optional dir initial)
  "Do consult-ripgrep with DIR and INITIAL, but without ignoring."
  (interactive "P")
  (consult--grep "Ripgrep"
                 #'consult--ripgrep-noignore-builder
                 (if dir dir t)  ;; Here the directory prompt is called by default to avoid searching from the project root
                 initial))

Narrowing which-key help without delay

After pressing consult-narrow-key, the which-key menu should appear immediately (#191).

(defun immediate-which-key-for-narrow (fun &rest args)
  (let* ((refresh t)
         (timer (and consult-narrow-key
                     (memq :narrow args)
                     (run-at-time 0.05 0.05
                                  (lambda ()
                                    (if (eq last-input-event (elt consult-narrow-key 0))
                                        (when refresh
                                          (setq refresh nil)
                                          (which-key--update))
                                      (setq refresh t)))))))
    (unwind-protect
        (apply fun args)
      (when timer
        (cancel-timer timer)))))
(advice-add #'consult--read :around #'immediate-which-key-for-narrow)

Shorten recent files in consult-buffer

NOTE: The vertico-truncate package provides this functionality in a more robust way. Using a package is recommended over copying large snippets to your Emacs configuration.

The recent files list which comprise one of the sources of consult-buffer are presented with full (abbreviated) path for completion by default. While this grants us a simple source and precise matching of the candidates, it comes with some drawbacks: i) candidates may get long enough so that the candidate gets truncated out of the window width; ii) even if truncation does not occur, marginalia annotations tend to get pushed away; iii) the full paths may match more than we’d like (depending on the use case). So one might prefer shortening the candidates from this source. See discussion at #713.

One first approach to that would be to simply use the file name of the candidate (disregarding the path). This makes for a simple and cheap shortening, with the disadvantage that some candidates may occur in duplicity, in which case the duplicates get shadowed in the completion. But, depending on the use case and preferences, it may be a valid option.

(defun my-consult--source-recentf-items ()
  (let ((ht (consult--buffer-file-hash))
        file-name-handler-alist ;; No Tramp slowdown please.
        items)
    (dolist (file recentf-list (nreverse items))
      ;; Emacs 29 abbreviates file paths by default, see
      ;; `recentf-filename-handlers'.
      (unless (eq (aref file 0) ?/)
        (setq file (expand-file-name file)))
      (unless (gethash file ht)
        (push (propertize
               (file-name-nondirectory file)
               'multi-category `(file . ,file))
              items)))))

(plist-put consult--source-recent-file
           :items #'my-consult--source-recentf-items)

A more polished approach, albeit more expensive, is to uniquify the candidates with non-common path parts.

(defun my-consult--source-recentf-items-uniq ()
  (let ((ht (consult--buffer-file-hash))
        file-name-handler-alist ;; No Tramp slowdown please.
        items)
    (dolist (file (my-recentf-list-uniq) (nreverse items))
      ;; Emacs 29 abbreviates file paths by default, see
      ;; `recentf-filename-handlers'.
      (unless (eq (aref (cdr file) 0) ?/)
        (setcdr file (expand-file-name (cdr file))))
      (unless (gethash (cdr file) ht)
        (push (propertize
               (car file)
               'multi-category `(file . ,(cdr file)))
              items)))))

(plist-put consult--source-recent-file
           :items #'my-consult--source-recentf-items-uniq)

(defun my-recentf-list-uniq ()
  (let* ((proposed (mapcar (lambda (f)
                             (cons (file-name-nondirectory f) f))
                           recentf-list))
         (recentf-uniq proposed)
         conflicts resol file)
    ;; collect conflicts
    (while proposed
      (setq file (pop proposed))
      (if (assoc (car file) conflicts)
          (push (cdr file) (cdr (assoc (car file) conflicts)))
        (if (assoc (car file) proposed)
            (push (list (car file) (cdr file)) conflicts))))
    ;; resolve conflicts
    (dolist (name conflicts)
      (let* ((files (mapcar (lambda (f)
                              ;; data structure:
                              ;; (file remaining-path curr-propos)
                              (list f
                                    (file-name-directory f)
                                    (file-name-nondirectory f)))
                            (cdr name)))
             (curr-step (mapcar (lambda (f)
                                  (file-name-nondirectory
                                   (directory-file-name (cadr f))))
                                files)))
        ;; Quick check, if there are no duplicates, we are done.
        (if (eq (length curr-step) (length (seq-uniq curr-step)))
            (setq resol
                  (append resol
                          (mapcar (lambda (f)
                                    (cons (car f)
                                          (file-name-concat
                                           (file-name-nondirectory
                                            (directory-file-name (cadr f)))
                                           (file-name-nondirectory (car f)))))
                                  files)))
          (while files
            (let (files-remain)
              (dolist (file files)
                (let ((curr-propos (caddr file))
                      (curr-part (file-name-nondirectory
                                  (directory-file-name (cadr file))))
                      (rest-path (file-name-directory
                                  (directory-file-name (cadr file))))
                      (curr-step
                       (mapcar (lambda (f)
                                 (file-name-nondirectory
                                  (directory-file-name (cadr f))))
                               files)))
                  (cond ((length= (seq-uniq curr-step) 1)
                         ;; If all elements of curr-step are equal, we skip
                         ;; this path part.
                         (push (list (car file)
                                     rest-path
                                     curr-propos)
                               files-remain))
                        ((member curr-part (cdr (member curr-part curr-step)))
                         ;; There is more than one curr-part in curr-step
                         ;; for this candidate.
                         (push (list (car file)
                                     rest-path
                                     (file-name-concat curr-part curr-propos))
                               files-remain))
                        (t
                         ;; There is no repetition of curr-part in curr-step
                         ;; for this candidate.
                         (push (cons (car file)
                                     (file-name-concat curr-part curr-propos))
                               resol)))))
              (setq files files-remain))))))
    ;; apply resolved conflicts
    (let (items)
      (dolist (file recentf-uniq (nreverse items))
        (let ((curr-resol (assoc (cdr file) resol)))
          (if curr-resol
              (push (cons (cdr curr-resol) (cdr file)) items)
            (push file items)))))))

Do not preview EXWM windows or Tramp buffers

NOTE: This section relates to the issues #178, #186, and #204. The recommended solution is to define an custom EXWM-specific buffer source without preview as is described in another section of the wiki.

Consult’s buffer preview functionality causes issues when used with EXWM. Because EXWM can only display an X buffer in one window at a time, previewing the buffer removes it from the original window. If the buffer is shown in another frame, it will also fail to restore the X buffer after finishing buffer selection.

Please take a look first at the custom EXWM-specific buffer source, which works around this problem in a clean way. Alternatively we can define a new buffer state function which checks whether the buffer is an EXWM buffer before deciding whether it should preview the contents or not.

(defun consult-buffer-state-no-exwm ()
  "Buffer state function that doesn't preview X buffers."
  (let ((orig-state (consult--buffer-state))
        (filter (lambda (action cand)
                  (and (or (eq action 'return)
                           (and-let* ((cand)
                                      (buffer (get-buffer cand)))
                             (not (eq 'exwm-mode (buffer-local-value 'major-mode buffer)))))
                       cand))))
    (lambda (action cand)
      (funcall orig-state action (funcall filter action cand)))))

(setq consult--source-buffer
      (plist-put consult--source-buffer :state #'consult-buffer-state-no-exwm))

This snippet defines a new :state function consult-buffer-state-no-exwm that we then use to replace the current :state function of consult--source-buffer.

Alternatively, if you wish to keep using previews with EXWM then add the following workaround to keep the minibuffer focused:

(defun consult-exwm-preview-fix (&rest _args)
  "Kludge to stop EXWM buffers from stealing focus during Consult previews."
  (when (derived-mode-p 'exwm-mode)
    (when-let ((mini (active-minibuffer-window)))
      (select-window (active-minibuffer-window)))))

(advice-add
    #'consult--buffer-preview :after #'consult-exwm-preview-fix)

Similarly one can exclude Tramp buffers from preview. This helps if one uses Tramp over an unstable connections where Tramp buffer switching can be slow or can hang (#224).

(defun consult-buffer-state-no-tramp ()
  "Buffer state function that doesn't preview Tramp buffers."
  (let ((orig-state (consult--buffer-state))
        (filter (lambda (action cand)
                  (if (and cand
                           (or (eq action 'return)
                               (let ((buffer (get-buffer cand)))
                                 (and buffer
                                      (not (file-remote-p (buffer-local-value 'default-directory buffer)))))))
                      cand
                    nil))))
    (lambda (action cand)
      (funcall orig-state action (funcall filter action cand)))))

(setq consult--source-buffer
      (plist-put consult--source-buffer :state #'consult-buffer-state-no-tramp))

Consult Buffer Sources

Point register source

We can add a register source to consult-buffer. It will show registers containing markers to specific places in buffers.

(defun consult--point-register-p (reg)
  "Return non-nil if REG is a point register."
  (markerp (cdr reg)))

(defvar-keymap consult-source-point-register
  `(:name     "Point Register"
    :narrow   (?r . "Register")
    :category consult-location
    :state
    ,(lambda ()
       (let ((state (consult--jump-state)))
         (lambda (action cand)
           (funcall state action (and cand (car (consult--get-location cand)))))))
    :enabled
    ,(lambda () (seq-some #'consult--point-register-p register-alist))
    :items
    ,(lambda () (consult-register--candidates #'consult--point-register-p)))
  "Point register source.")

(add-to-list 'consult-buffer-sources 'consult-source-point-register 'append)

Source for files in current directory

(defvar +consult-source-neighbor-file
  `(:name     "File in current directory"
    :narrow   ?.
    :category file
    :face     consult-file
    :history  file-name-history
    :state    ,#'consult--file-state
    :new      ,#'consult--file-action
    :items
    ,(lambda ()
       (let ((ht (consult--buffer-file-hash)) items)
         (dolist (file (completion-pcm--filename-try-filter
                        (directory-files "." 'full "\\`[^.]" nil 100))
                       (nreverse items))
           (unless (or (gethash file ht) (not (file-regular-p file)))
             (push (file-name-nondirectory file) items))))))
  "Neighboring file source for `consult-buffer'.")

(unless (memq '+consult-source-neighbor-file consult-buffer-sources)
  (let ((p (member 'consult--source-buffer consult-buffer-sources)))
    (setcdr p (cons '+consult-source-neighbor-file (cdr p)))))

ERC Buffers

ERC is an IRC client. You can define a source containing only ERC buffers (#290).

(autoload 'erc-buffer-list "erc")

(defvar erc-buffer-source
  `(:name     "ERC"
              :hidden   t
              :narrow   ?e
              :category buffer
              :state    ,#'consult--buffer-state
              :items    ,(lambda () (mapcar #'buffer-name (erc-buffer-list)))))

(add-to-list 'consult-buffer-sources 'erc-buffer-source 'append)

If like me you have a dedicated tab for ERC using the built-in tab-bar-mode (starting 27.1), you can use this function to have initial narrowing under the “ERC” tab solely, so as to display the ERC related candidates (#290).

(defun consult-initial-narrow ()
  (when (and (eq this-command #'consult-buffer)
             (string-equal "ERC" (alist-get 'name (alist-get 'current-tab (tab-bar-tabs)))))
    (setq unread-command-events (append unread-command-events (list ?e 32)))))

(add-hook 'minibuffer-setup-hook #'consult-initial-narrow)

rcirc Buffers

rcirc is an alternative IRC client. Since rcirc doesn’t provide a way of retrieving its buffers, we need to create one:

(defun consult-rcirc--buffer-list ()
  "Return the list of current rcirc buffers."
  (let ((rcirc-buffers
         (cl-remove-if-not
          (lambda (buffer)
            (with-current-buffer buffer
              (eq major-mode 'rcirc-mode)))
          (buffer-list))))
    (cl-remove-duplicates
      rcirc-buffers
     :test #'equal)))

(defvar rcirc-buffer-source
  `(:name     "rcirc"
              :hidden   t
              :narrow   ?r
              :category buffer
              :state    ,#'consult--buffer-state
              :items    ,(lambda () (mapcar #'buffer-name (consult-rcirc--buffer-list)))))

(add-to-list 'consult-buffer-sources 'rcirc-buffer-source 'append)

Circe Buffers

Circe is an alternative IRC client. Similar to ERC, Consult buffer sources can be defined. Because of the way that circe separates chat and server buffers, the :items function is a bit more involved:

(require 'cl-lib)
(autoload 'circe-server-buffers "circe")
(autoload 'circe-server-chat-buffers "circe")

(defun circe-all-buffers ()
  (cl-loop with servers = (circe-server-buffers)
           for server in servers
           collect server
           nconc
           (with-current-buffer server
             (cl-loop for buf in (circe-server-chat-buffers)
                      collect buf))))

(defvar circe-buffer-source
  `(:name     "circe"
              :hidden   t
              :narrow   ?c
              :category buffer
              :state    ,#'consult--buffer-state
              :items    ,(lambda () (mapcar #'buffer-name (circe-all-buffers)))))

(add-to-list 'consult-buffer-sources 'circe-buffer-source 'append)

Eww Bookmarks

Since Emacs 28, Eww makes use of the standard Emacs bookmark infrastructure. The old-style Eww bookmarks can be integrated with Consult as follows. See discussion in #347.

(require 'eww)

(defvar consult--source-eww
  (list
   :name     "Eww"
   :narrow   ?e
   :action   (lambda (bm)
               (eww-browse-url (get-text-property 0 'url bm)))
   :items    (lambda ()
               (eww-read-bookmarks)
               (mapcar (lambda (bm)
                         (propertize
                          (format "%s (%s)"
                                  (plist-get bm :url)
                                  (plist-get bm :title))
                          'url (plist-get bm :url)))
                       eww-bookmarks))))

(add-to-list 'consult-buffer-sources 'consult--source-eww 'append)

EXWM Buffers

To group all EXWM windows together, we can create an +consult-source-exwm and add it to the list of buffer sources. Preview is disabled for the EXWM buffers here since X11 buffers cannot be duplicated. We also hide EXWM buffers from the other buffer sources.

(defvar +consult-exwm-filter "\\`\\*EXWM")
(add-to-list 'consult-buffer-filter +consult-exwm-filter)

(defvar +consult-source-exwm
  `(:name      "EXWM"
    :narrow    ?x
    ;; :hidden t
    :category  buffer
    :face      consult-buffer
    :history   buffer-name-history
    ;; Specify either :action or :state
    :action    ,#'consult--buffer-action ;; No preview
    ;; :state  ,#'consult--buffer-state  ;; Preview
    :items
    ,(lambda () (consult--buffer-query
                 :sort 'visibility
                 :as #'buffer-name
                 :exclude (remq +consult-exwm-filter consult-buffer-filter)
                 :mode 'exwm-mode)))
  "EXWM buffer source.")

Bufler

Add a consult-buffer source to group buffers from the current bufler workspace.

(defvar consult--bufler-workspace+
  `(:name "Workspace"
    :narrow ?w
    :category buffer
    :face consult-buffer
    :history  buffer-name-history
    :state    ,#'consult--buffer-state
    :enabled  ,(lambda () (frame-parameter nil 'bufler-workspace-path))
    :items
    ,(lambda ()
       (let ((bufler-vc-state nil))
         (mapcar #'buffer-name
                 (mapcar #'cdr
                         (bufler-buffer-alist-at
                          (frame-parameter nil 'bufler-workspace-path)
                          :filter-fns bufler-filter-buffer-fns))))))
  "Bufler workspace buffers source for `consult-buffer'.")

(with-eval-after-load 'consult
  (push #'consult--bufler-workspace+ consult-buffer-sources))

Dogears

Dogears source for Consult. See #430.

(defvar consult--source-dogears
  (list :name     "Dogears"
        :narrow   ?d
        :category 'dogears
        :items    (lambda ()
                    (mapcar
                     (lambda (place)
                       (propertize (dogears--format-record place)
                                   'consult--candidate place))
                     dogears-list))
        :action   (lambda (cand)
                    (dogears-go (get-text-property 0 'consult--candidate cand)))))

(defun consult-dogears ()
  (interactive)
  (consult--multi '(consult--source-dogears)))

Perspective

Use consult-buffer with perspective-el. This would hide the default consult--source-buffer, and show the list of perspective buffers on the top

(consult-customize consult--source-buffer :hidden t :default nil)

(defvar consult--source-perspective
  (list :name     "Perspective"
        :narrow   ?s
        :category 'buffer
        :state    #'consult--buffer-state
        :default  t
        :items    #'persp-get-buffer-names))

(push consult--source-perspective consult-buffer-sources)

Bookmark views

Sources can be added directly to the consult-buffer-source list for convenience. For example views/perspectives can be added to the list of virtual buffers from a library like bookmark-view.

;; Configure new bookmark-view source
(add-to-list 'consult-buffer-sources
              (list :name     "View"
                    :narrow   ?v
                    :category 'bookmark
                    :face     'font-lock-keyword-face
                    :history  'bookmark-view-history
                    :action   #'consult--bookmark-action
                    :items    #'bookmark-view-names)
              'append)

;; Modify bookmark source, such that views are hidden
(setq consult--source-bookmark
      (plist-put
       consult--source-bookmark :items
       (lambda ()
         (bookmark-maybe-load-default-file)
         (mapcar #'car
                 (seq-remove (lambda (x)
                               (eq #'bookmark-view-handler
                                   (alist-get 'handler (cdr x))))
                             bookmark-alist)))))

Commands

consult-line-literal which matches only literally

consult-line uses the completion-styles for matching. One can write a wrapper around consult-line which adjusts the completion styles to the desired configuration.

;; Use the `substring` completion style
(defun consult-line-literal ()
  (interactive)
  (let ((completion-styles '(substring))
        (completion-category-defaults nil)
        (completion-category-overrides nil))
    (consult-line)))

;; Use the `orderless` completion style, restricted to `orderless-literal`
(defun consult-line-literal ()
  (interactive)
  (let ((completion-styles '(orderless))
        (orderless-matching-styles '(orderless-literal))
        (completion-category-defaults nil)
        (completion-category-overrides nil))
    (consult-line)))

Start consult-line search with symbol at point

The symbol at point can be passed as initial argument to consult-line.

(defun consult-line-symbol-at-point ()
  (interactive)
  (consult-line (thing-at-point 'symbol)))

Start consult-ripgrep search with active region

Conditionally use the active region as the initial parameter value for consult-ripgrep.

(defun wrapper/consult-ripgrep (&optional dir given-initial)
  "Pass the region to consult-ripgrep if available.

DIR and GIVEN-INITIAL match the method signature of `consult-wrapper'."
  (interactive "P")
  (let ((initial
         (or given-initial
             (when (use-region-p)
               (buffer-substring-no-properties (region-beginning) (region-end))))))
    (consult-ripgrep dir initial)))

Restart consult-ripgrep in parent directory

We can define a command to restart the current consult-ripgrep search in the parent directory. See issue 596 for the background.

(defun consult-ripgrep-up-directory ()
  (interactive)
  (let ((parent-dir (file-name-directory (directory-file-name default-directory))))
    (when parent-dir
        (run-at-time 0 nil
                     #'consult-ripgrep
                     parent-dir
                     (ignore-errors
                       (buffer-substring-no-properties
                        (1+ (minibuffer-prompt-end)) (point-max))))))
  (minibuffer-quit-recursive-edit))

(consult-customize
 consult-ripgrep
 :keymap (let ((map (make-sparse-keymap)))
           (define-key map (kbd "M-l") #'consult-ripgrep-up-directory)
           map))

consult-ripgrep-or-line (counsel-grep-or-swiper equivalent)

If consult-line is slow in large buffers, this may be useful. Without native compilation, I can increase the limit significantly. With native compilation, consult-line is near instant in my largest org file already.

  (defcustom my/consult-ripgrep-or-line-limit 300000
    "Buffer size threshold for `my/consult-ripgrep-or-line'.
When the number of characters in a buffer exceeds this threshold,
`consult-ripgrep' will be used instead of `consult-line'."
    :type 'integer)

  (defun my/consult-ripgrep-or-line ()
    "Call `consult-line' for small buffers or `consult-ripgrep' for large files."
    (interactive)
    (if (or (not buffer-file-name)
            (buffer-narrowed-p)
            (ignore-errors
              (file-remote-p buffer-file-name))
            (jka-compr-get-compression-info buffer-file-name)
            (<= (buffer-size)
                (/ my/consult-ripgrep-or-line-limit
                   (if (eq major-mode 'org-mode) 4 1))))
        (consult-line)
      (when (file-writable-p buffer-file-name)
        (save-buffer))
      (let ((consult-ripgrep-args
             (concat consult-ripgrep-args
                     ;; filter to desired filename
                     " -g "
                     (shell-quote-argument (file-name-nondirectory buffer-file-name))
                     " ")))
        (consult-ripgrep))))

Access directories of text notes files

hrm-notes is a command to access text file notes from several directories easily. It uses consult-multi and includes embark integration. It’s a simple deft-like command.

Using find in the minibuffer

The following command, meant to be called in the minibuffer when it is reading a file name, switches from the usual hierarchical browsing of the file system to a consult-find session.

(defun consult-find-for-minibuffer ()
  "Search file with find, enter the result in the minibuffer."
  (interactive)
  (let* ((enable-recursive-minibuffers t)
         (default-directory (file-name-directory (minibuffer-contents)))
         (file (consult--find
                (replace-regexp-in-string
                 "\\s-*[:([].*"
                 (format " (via find in %s): " default-directory)
                 (minibuffer-prompt))
                (consult--find-make-builder)
                (file-name-nondirectory (minibuffer-contents)))))
    (delete-minibuffer-contents)
    (insert (expand-file-name file default-directory))
    (exit-minibuffer)))

It is convenient to add a category-specific keybinding to this command.

Including file recently used by other programs

If you find yourself using other programs with Emacs, it can be helpful to include files used by other programs in the candidate lists of commands like consult-recent-file and consult-buffer. That way, you never have any mental hiccups when trying to open files in Emacs that you recently opened in a different program. Instead, you simply use the same interface with which you are already familiar.

I put this code into consult-xdg-recent-files, which should be installable with straight. Maybe this saves someone the maintenance.

The way to access this information is generally specific to each system. Please update this section for other systems, if you find this feature useful.

In Linux (or, more specifically, on systems that comply with the XDG specification), these files are listed in the file recently-used.xbel, which is found in the directory ~/.local/share or the location described by the environment variable XDG_DATA_HOME.

We can access the data in this file using libraries built-in with Emacs, namely url-util.el, dom.el, and one of xml.c or xml.el.

(require 'dom)
(require 'url-util)
(require 'xml)

(defun consult--xdg-recent-file-list ()
  "Get a list of recently used files on XDG-compliant systems.

This function extracts a list of files from the file
`recently-used.xbel' in the folder `xdg-data-home'.

For more information on this specification, see
https://www.freedesktop.org/wiki/Specifications/desktop-bookmark-spec/"
  (let ((data-file (expand-file-name "recently-used.xbel" (xdg-data-home)))
        (xml-parsing-func (if (libxml-available-p)
                              #'libxml-parse-xml-region
                            #'xml-parse-region)))
    (if (file-readable-p data-file)
        (delq nil
              (mapcar (lambda (bookmark-node)
                        (when-let ((local-path (string-remove-prefix
                                                "file://"
                                                (dom-attr bookmark-node 'href))))
                          (let ((full-file-name (decode-coding-string
                                                 (url-unhex-string local-path)
                                                 'utf-8)))
                            (when (file-exists-p full-file-name)
                              full-file-name))))
                      (nreverse (dom-by-tag (with-temp-buffer
                                              (insert-file-contents data-file)
                                              (funcall xml-parsing-func
                                                       (point-min)
                                                       (point-max)))
                                            'bookmark))))
      (message "consult: List of XDG recent files not found")
      '())))

If using multiple systems, then it is good to wrap such a feature in a dispatching function.

(require 'cl-lib)

(defun consult--recent-system-files ()
  "Return a list of files recently used by the system."
  (cl-case system-type
    (gnu/linux
     (consult--xdg-recent-file-list))
    (t
     (message "consult-recent-file: \"%s\" currently unsupported"
              system-type)
     '())))

Generally, one would want to sort these files from most recently used to least recently used. A file’s modification time works well for this, and isn’t disturbed when Emacs accesses the file.

(defun consult--recent-files-sort (file-list)
  "Sort the FILE-LIST by modification time, from most recent to least recent."
  (thread-last
      file-list
    ;; Use modification time, since getting file access time seems to count as
    ;; accessing the file, ruining future uses.
    (mapcar (lambda (f)
              (cons f (file-attribute-modification-time (file-attributes f)))))
    (seq-sort (pcase-lambda (`(,f1 . ,t1) `(,f2 . ,t2))
                ;; Want existing, most recent, local files first.
                (cond ((or (not (file-exists-p f1))
                           (file-remote-p f1))
                       nil)
                      ((or (not (file-exists-p f2))
                           (file-remote-p f2))
                       t)
                      (t (time-less-p t2 t1)))))
    (mapcar #'car)))

To mix these candidates with those found in the variable recentf-list, we only need to filter according the function recentf-include-p.

(defun consult--recent-files-mixed-candidates ()
  "Return a list of files recently used by Emacs and the system.

These files are sorted by modification time, from most recent to least."
  (thread-last
      (consult--recent-system-files)
    (seq-filter #'recentf-include-p)
    (append (mapcar #'substring-no-properties recentf-list))
    delete-dups
    (consult--recent-files-sort)))

To include the mixed candidates in consult-recent-file, we can slightly modify its definition.

(defcustom consult-include-system-recent-files nil
  "Whether to include files used by other programs in `consult-recent-file'."
  :type 'boolean
  :group 'consult)

;;;###autoload
(defun consult-recent-file ()
  "Find recent using `completing-read'."
  (interactive)
  (find-file
   (consult--read
    (or (mapcar #'abbreviate-file-name
                (if consult-include-system-recent-files
                    (consult--recent-files-mixed-candidates)
                  recentf-list))
        (user-error "No recent files"))
    :prompt "Find recent file: "
    :sort nil
    :require-match t
    :category 'file
    :state (consult--file-preview)
    :history 'file-name-history)))

To include these candidates in consult-buffer, we can add a source to the variable consult-buffer-sources.

(defvar consult--source-system-file
  `(:name     "System file"
              :narrow   ?F
              :category file
              :face     consult-file
              :history  file-name-history
              :action   ,#'consult--file-action
              :items
              ,(lambda ()
                 (let ((ht (consult--buffer-file-hash)))
                   (mapcar #'abbreviate-file-name
                           (seq-remove (lambda (x) (gethash x ht))
                                       (consult--recent-system-files))))))
  "Recent system file candidate source for `consult-buffer'.")

(defvar consult--source-mixed-file
  `(:name     "File"
              :narrow   ?f
              :category file
              :face     consult-file
              :history  file-name-history
              :action   ,#'consult--file-action
              :items
              ,(lambda ()
                 (let ((ht (consult--buffer-file-hash)))
                   (mapcar #'abbreviate-file-name
                           (seq-remove (lambda (x) (gethash x ht))
                                       (consult--recent-files-mixed-candidates))))))
  "File candidate source for `consult-buffer', including system files.
This is meant as a replacement for `consult--source-file'.")

;; Example: using the "mixed" source in `consult-buffer':
(setq consult-buffer-sources
      '( consult--source-hidden-buffer
         consult--source-buffer
         consult--source-mixed-file
         consult--source-bookmark
         consult--source-project-buffer
         consult--source-project-file))

Org: select both agenda files and agenda headings

The following configuration uses the consult--multi API to present both the user’s agenda files, and the headings in those files.

;; Tested with consult commit e222aacb656161233931c4ff27d7625f31f3aaf9
;; `consult-org--headings' adds different text properties before this
;; commit, so you'd have to tweak this code

(defun my/consult-org-agenda-multi ()
  "Jump to an Org agenda heading or file."
  (interactive)
  (require 'consult-org)
  (consult--multi
   (list
    (my/consult--file-relative-source (org-agenda-files) org-directory)
    (my/consult-org--heading-source '(nil nil agenda)))
   :require-match t
   :sort t))

(defun my/consult--file-relative-source (filenames dir)
  "File candidate source; FILENAMES are presented relative to DIR."
  `(:name "Agenda File"
    :category file
    :narrow ?f ; type '< f' to narrow to filenames
    :history file-name-history
    :state ,#'consult--file-state
    :items
    ,(mapcar (lambda (f)
               (propertize (file-relative-name f dir)
                           'multi-category (cons 'file f)))
             filenames)))

(defun my/consult-org--heading-source
    (items-args)
  "Generate Org heading candidate source.
ITEMS-ARGS is a list of arguments passed to `consult--org-headings' to generate
the list of candidates."
  `(:name "Agenda Heading"
    :category org-heading
    :items ,(apply #'consult-org--headings items-args)
    :history consult-org--history
    :narrow ,(consult-org--narrow)
    :state ,#'my/consult-org--heading-state
    :annotate ,(my/consult-org--annotate)))

(defun my/consult-org--heading-state ()
  "State function for Org headings with preview."
  (consult--state-with-return
   (my/consult-org--heading-preview)
   #'my/consult-org--heading-jump))

(defun my/consult-org--heading-preview ()
  "The preview function used if selecting from a list of Org headings.
Simply wraps `consult--jump-preview'."
  ;; the closure that `consult--jump-preview' returns must be retained for the
  ;; duration of completion, since it stores the list of overlays to remove
  ;; ('restore' variable)
  (let ((preview-fn (consult--jump-preview)))
    (lambda (action cand)
      (funcall preview-fn action
               (when cand (get-text-property 0 'org-marker cand))))))

(defun my/consult-org--heading-jump (heading)
  "Jump to Org HEADING.
Simply wraps `consult--jump'."
  (consult--jump (get-text-property 0 'org-marker heading)))

(defun my/consult-org--annotate ()
  "Generate annotation function for Org headings.
Also adds the filename relative to `org-directory' as an annotation."
  (let ((ann-maxlens (list 0 0 0))) ; 1 elt per annotation type supported
    (lambda (cand)
      (let* ((props (get-text-property 0 'consult-org--heading cand))
             (kwd (or (cadr props) ""))
             (prio (if-let ((prio (caddr props)))
                       (format #("[#%c]" 0 5 (face org-priority)) prio)
                     ""))
             (buf (cdddr props))
             (file (file-relative-name (buffer-file-name (get-buffer buf))
                                       (file-truename org-directory)))
             (anns (list kwd prio file)))
        ;; pad annotations so they're aligned into columns
        (dotimes (i (length anns))
          (when-let ((str (nth i anns)))
            (let ((len (length str))
                  (prevlen (nth i ann-maxlens))
                  maxlen)
              (if (>= prevlen len)
                  (setq maxlen prevlen)
                (setq maxlen len)
                (setcar (nthcdr i ann-maxlens) maxlen))
              (setcar (nthcdr i anns) (string-pad str maxlen)))))
        (consult--annotate-align cand (mapconcat #'identity anns " "))))))

Org clock

The following is a simple command to select and clock into an agenda entry.

(defun consult-clock-in ()
  "Clock into an Org agenda heading."
  (interactive)
  (save-window-excursion
    (consult-org-agenda)
    (org-clock-in)))

(consult-customize consult-clock-in
                   :prompt "Clock in: "
                   :preview-key "M-.")

Below is a fancier version with the following perks, which you may pick and choose:

  • Instead of offering agenda entries, offer headings from all files that have a recent clock entry.
  • Sort recent clock entries separately under a *Recent* group.
  • With a prefix argument, resolve dangling clocks and ask for a time to clock into the selected task.
(setq org-clock-persist t)
(with-eval-after-load 'org
  (org-clock-persistence-insinuate))

(defun consult-clock-in (&optional match scope resolve)
  "Clock into an Org heading."
  (interactive (list nil nil current-prefix-arg))
  (require 'org-clock)
  (org-clock-load)
  (save-window-excursion
    (consult-org-heading
     match
     (or scope
         (thread-last org-clock-history
           (mapcar 'marker-buffer)
           (mapcar 'buffer-file-name)
           (delete-dups)
           (delq nil))
         (user-error "No recent clocked tasks")))
    (org-clock-in nil (when resolve
                        (org-resolve-clocks)
                        (org-read-date t t)))))

(consult-customize consult-clock-in
                   :prompt "Clock in: "
                   :preview-key "M-."
                   :group
                   (lambda (cand transform)
                     (let* ((marker (get-text-property 0 'consult--candidate cand))
                            (name (if (member marker org-clock-history)
                                      "*Recent*"
                                    (buffer-name (marker-buffer marker)))))
                       (if transform (substring cand (1+ (length name))) name))))

Note that these commands can also be used as an Embark action.

Org capture

Normally, an Org capture target specifies a fixed file or heading within a file as its target. The following example shows how to define a capture target that first queries an agenda entry using consult-org-headline, and then places the capture directly beneath it.

(defun consult-org-capture-target (scope)
  "Choose a capture target interactively.
This function returns a value suitable for use as the `target'
entry of `org-capture-templates'.  SCOPE is as in `org-map-entries'."
  (list 'function
        (lambda ()
          (let ((consult--read-config `((,this-command
                                         :prompt "Capture target: "
                                         :preview-key "M-."))))
            (set-buffer (save-window-excursion
                          (consult-org-heading nil scope)
                          (current-buffer)))))))

(add-to-list 'org-capture-templates
             `(("c" "Consult..." entry ,(consult-org-capture-target 'agenda)
                "* TODO %?\n  %i" :prepend t)))

You may also wish to have a direct keybinding to this capture type, instead of going though the M-x org-capture menu. In this case, use the following:

(defun consult-org-capture ()
  (interactive)
  (org-capture nil "c"))

isearch-like backward/forward consult-line

Type C-s to search forward and C-r to search backward. Requires the vertico-reverse extension to be enabled.

(defun my/consult-line-forward ()
  "Search for a matching line forward."
  (interactive)
  (consult-line))

(defun my/consult-line-backward ()
  "Search for a matching line backward."
  (interactive)
  (advice-add 'consult--line-candidates :filter-return 'reverse)
  (vertico-reverse-mode +1)
  (unwind-protect (consult-line)
    (vertico-reverse-mode -1)
    (advice-remove 'consult--line-candidates 'reverse)))

(with-eval-after-load 'consult
  (consult-customize my/consult-line-backward
                     :prompt "Go to line backward: ")
  (consult-customize my/consult-line-forward
                     :prompt "Go to line forward: "))

(global-set-key (kbd "C-s") 'my/consult-line-forward)
(global-set-key (kbd "C-r") 'my/consult-line-backward)

Completing a hierarchical outline

This is based on the command selectrum-outline, minimally modified to use consult--read.

(defvar my-consult-outline-path-history nil
  "History of chosen headings for ‘my-consult-outline-path’.")

(defcustom my-consult-outline-path-formats
  ;; Groups: (1) level determinant, (2) heading text.
  ;; The top level is 0, for a zero-length determinant.
  `((emacs-lisp-mode
     . "^;;;\\(?1:;*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (diff-mode
     ;; We just need a zero-length thing to match the file line
     ;; and a one-length thing to match the section line.
     ;; This works even with `diff-font-lock-prettify' enabled.
     . ,(rx (or (seq line-start (group-n 1 "")
                     "diff " (0+ nonl) "b/" (group-n 2 (+? nonl))
                     string-end)
                (seq line-start "@" (group-n 1 "@")
                     " " (group-n 2 (0+ nonl)) string-end))))
    (lisp-mode
     . "^;;;\\(?1:;*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (lua-mode
     . "^---\\(?1:-*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (gfm-mode ; Github Flavored Markdown
     . "^#\\(?1:#*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (markdown-mode
     . "^#\\(?1:#*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (outline-mode
     . "^\\*\\(?1:\\**\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    ;; For Org, see also ‘org-goto’.
    (org-mode
     . "^\\*\\(?1:\\**\\)[[:blank:]]+\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (python-mode
     . "^##\\(?1:\\**\\|#*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (shortdoc-mode
     . "^\\(?1:\\)\\(?2:[A-Z].*$\\)"))
  "Alist of regexps used for identifying outline headings in each major mode.

The ‘car’ of an item in the list should be a symbol of the major mode.
The ‘cdr’ should be a regular expression with two required match groups:
1. Match group 1, whose length determines the outline level of that heading.
   For best formatting, the top level should be level 0 for zero length.
2. Match group 2, which is the actual heading text.

A heading is assumed to be on only one line."
  :group 'consult
  :type '(alist
          :key-type (symbol :tag "Major mode symbol")
          :value-type (string :tag "Regexp")))

;;;###autoload
(defun my-consult-outline-path ()
  "Jump to a heading.  Regexps are pre-defined.  Obeys narrowing."
  (interactive)
  (if-let ((heading-regexp (alist-get major-mode my-consult-outline-path-formats)))
      (let ((candidates)
            (default-heading)
            (initial-line-number (line-number-at-pos (point))))
        (save-excursion
          (goto-char (point-min))
          (let* ((line-number (line-number-at-pos (point)))
                 (point-max (point-max))
                 (beg (point))
                 (end (line-end-position))

                 (backwards-prefix-list)
                 (prev-heading-text)
                 (prev-heading-level)
                 (heading-text)
                 (heading-level)

                 (formatted-heading))
            (save-match-data
              (while (< end point-max)
                (let ((text-line (buffer-substring beg end)))
                  (when (string-match heading-regexp text-line)
                    (setq prev-heading-text  heading-text
                          prev-heading-level heading-level
                          heading-text  (match-string-no-properties 2 text-line)
                          heading-level (- (match-end 1) (match-beginning  1)))

                    ;; Decide whether to update the prefix list and the previous
                    ;; heading level.
                    (let ((prev-heading-level (or prev-heading-level heading-level)))
                      (cond
                       ;; If we've moved to a greater level (further down the tree),
                       ;; add the previous heading to the heading prefix list so
                       ;; that we can prepend it to the current heading when
                       ;; formatting.
                       ((> heading-level prev-heading-level)
                        (push prev-heading-text backwards-prefix-list))
                       ((< heading-level prev-heading-level)
                        ;; Otherwise, if we've moved to a lower level (higher up the
                        ;; tree), and need to remove the most recently added prefix
                        ;; from the list (i.e., go from '(c b a) back to '(b a)).
                        (cl-callf2 nthcdr (- prev-heading-level heading-level)
                                   backwards-prefix-list))))

                    ;; If needed, set default candidate.
                    (when (and (null default-heading)
                               (> line-number initial-line-number))
                      (setq default-heading formatted-heading))

                    (setq formatted-heading
                          (propertize
                           (concat (string-join (reverse backwards-prefix-list) "/")
                                   (and backwards-prefix-list "/")
                                   heading-text)
                           'line-number line-number))

                    (push formatted-heading candidates)))
                (cl-incf line-number)
                (forward-line 1)
                (setq beg (point)
                      end (line-end-position))))

            (unless default-heading
              (setq default-heading formatted-heading))))

        (cl-flet ((ln (str) (get-text-property 0 'line-number str)))
          (let* ((line-number-format
                  (format "L%%0%dd: "
                          (length (number-to-string (ln (car candidates))))))
                 (affixate-func
                  (lambda (cand)
                    (list cand (propertize (format line-number-format (ln cand))
                                           'face 'completions-annotations)
                          "")))
                 (lookup-fn (lambda (selected candidates &rest _)
                              (consult--lookup-prop 'line-number
                                                    selected candidates)))
                 (chosen-line (consult--read (nreverse candidates)
                                             :prompt "Jump to heading: "
                                             :require-match t
                                             :history 'my-consult-outline-path-history
                                             ;; TODO: Want to select default
                                             ;;       without moving it to the
                                             ;;       top of the list.
                                             ;; :default default-heading
                                             :annotate affixate-func
                                             :lookup lookup-fn
                                             :sort nil)))

            ;; Push mark, in case we want to return to current location.  This
            ;; needs to happen /after/ the user has made it clear that they
            ;; want to go somewhere.
            (push-mark (point) t)
            ;; Move to beginning of chosen line.
            (forward-line (- chosen-line initial-line-number))
            (beginning-of-line-text 1)
            ;; Return non-nil for advice combinator `after-while'.
            t)))
    (call-interactively #'consult-outline)))

Completing tabs from tab-bar with preview

This can be useful if you want to set tab-bar-show to nil

(defun +tab-bar--make-completion-list (tab-list)
  "Return completion list of strings formatted from TAB-LIST."
  (mapcar (lambda (tab)
            (let ((index (1+ (tab-bar--tab-index tab)))
                  (name (alist-get 'name tab)))
              (format "%d %s" index name)))
          tab-list))

(defun +tab-bar--completion-list-recent ()
  "Return completion list of recent tabs (current not included)."
  (+tab-bar--make-completion-list (tab-bar--tabs-recent)))

(defun +tab-bar--index-from-candidate (cand)
  "Return prefix index of CAND."
  (let ((match (string-match "^[[:digit:]]+" cand)))
    (when match
      (string-to-number (match-string match cand)))))

(defun +tab-bar--tab-from-index (index)
  "Return tab from `(tab-bar-tabs)' by index of CAND."
  (when index
    (nth (1- index) (tab-bar-tabs))))

(defun +consult--tab-preview ()
  "Preview function for tabs."
  (let ((orig-wc (current-window-configuration)))
    (lambda (action cand)
      (if (eq action 'exit)
          (set-window-configuration orig-wc nil t)
        (when cand
          (let* ((index (+tab-bar--index-from-candidate cand))
                 (tab (+tab-bar--tab-from-index index)))
            (when tab
              (if (eq (car tab) 'current-tab)
                  (set-window-configuration orig-wc nil t)
                (set-window-configuration (alist-get 'wc tab) nil t)))))))))

(defun +consult--tab-annotate (cand)
  "Annotate current tab."
  (when (equal (car (+tab-bar--tab-from-index (+tab-bar--index-from-candidate cand))) 'current-tab)
    "Current"))

(defun +consult--tab-action-select (cand)
  "Select tab from CAND."
  (tab-bar-select-tab (+tab-bar--index-from-candidate cand)))

(defvar +consult--tab-history
  "History of tab completion selections.")

(defvar +consult--source-tab-recent
  (list :name "Tab"
        :category 'tab
        :narrow ?t
        :default t
        :history '+consult--tab-history
        :items #'+tab-bar--completion-list-recent
        :annotate #'+consult--tab-annotate
        :action #'+consult--tab-action-select
        :state  #'+consult--tab-preview))

(defun +consult-tab ()
  "Select tab with completion and preview."
  (interactive)
  (consult--multi '(+consult--source-tab-recent) :prompt "Select tab: "))

(defun +consult-tab-close ()
  "Select tab to close it."
  (interactive)
  (tab-bar-close-tab (+tab-bar--index-from-candidate (car (consult--multi '(+consult--source-tab-recent) :prompt "Close tab: ")))))

Another approach to tab-bar completion with marginalia

Due to the fact that it is almost impossible to properly complete tabs from a tab bar by their name if their names are not unique, the only thing left is to complete their indexes. This approach is an improvement on the code from the previous section. Only now we use marginalia to display the name.

Here is the result

;; -*- lexical-binding: t; -*-

(require 'consult)
(require 'marginalia)

(defvar +consult--tab-index-current-tab-name nil
  "The name of the current tab. Needed for marginalia annotations when previewing tabs.
Because we are changing the current window configuration when previewing tabs, we are
also changing the name of the current tab unless it's not an explicit name. To prevent
this, we can store the name of the current tab before calling consult command and use
this saved name in marginalia annotations of the current tab.")

(defvar +consult--tab-index-current-tab-bufs nil
  "List of current tab buffer names. Needed for marginalia annotations when previewing tabs.
Because we are changing the current window configuration when previewing tabs, we need to
save the current list of buffers displayed in windows before calling consult command and
use this saved list in marginalia annotations of the current tab.")

(defun +marginalia-annotate-tab-index (cand)
  "Modified version of `marginalia-annotate-tab' suited for tab-index completion."
  (let* ((tab (nth (1- (string-to-number cand)) (tab-bar-tabs)))
         (current-p (memq 'current-tab tab))
         (ws (alist-get 'ws tab))
         (bufs (if current-p
                   +consult--tab-index-current-tab-bufs
                 (window-state-buffers ws))))
    ;; NOTE: When the buffer key is present in the window state
    ;; it is added in front of the window buffer list and gets duplicated.
    (unless current-p
      (when (cadr (assq 'buffer ws)) (pop bufs)))
    (marginalia--fields
     ;; Tab name
     ((if current-p
          +consult--tab-index-current-tab-name
        (alist-get 'name tab))
      :face (if current-p 'marginalia-on 'marginalia-key)
      :width 15
      :truncate 15)
     ;; Window count
     ((if (cdr bufs)
          (format "%d windows" (length bufs))
        "1 window ")
      :face 'marginalia-size
      :width 15)
     ;; List of buffers
     ((string-join bufs " \t ")
      :face 'marginalia-documentation))))

(add-to-list 'marginalia-annotator-registry '(tab-index +marginalia-annotate-tab-index))

(defun +consult--tab-index-preview ()
  "Preview function for tab-index."
  (let ((orig-wc (current-window-configuration)))
    (lambda (action cand)
      (if (eq action 'exit)
          (set-window-configuration orig-wc nil t)
        (when cand
          (set-window-configuration
           (alist-get 'wc (nth (1- (string-to-number cand))
                               (tab-bar-tabs))
                      ;; default to original wc if
                      ;; there is no tab wc (usually current tab)
                      orig-wc)
           nil t))))))

(defvar +consult--source-tab-index
  (list :name "Tab"
        :category 'tab-index
        :default t
        :narrow ?t
        :state #'+consult--tab-index-preview
        :items (lambda ()
                 (mapcar #'number-to-string
                         (number-sequence 1 (length (tab-bar-tabs))))))
  "Source of all tab indexes starting from 1.")

(defun +consult--tab-index (&optional prompt)
  "Prompt for tab selection and return selected candidate as number.
Replace prompt with PROMPT if specified."
  ;; Marginalia integration
  (let (;; Align annotations as close to index as possible
        (marginalia-align-offset -18)
        ;; Save curret tab name
        (+consult--tab-index-current-tab-name (alist-get 'name (tab-bar--current-tab)))
        ;; Save current window buffer list
        (+consult--tab-index-current-tab-bufs (mapcar #'buffer-name
                                                      (mapcar #'window-buffer
                                                              (window-list)))))
    (string-to-number (car (consult--multi '(+consult--source-tab-index)
                                           ;; disable sorting
                                           :sort nil
                                           :require-match t
                                           :prompt (or prompt "Select tab: "))))))

;;;###autoload
(defun +consult-tab ()
  "Select tab and switch to it."
  (interactive)
  (tab-bar-select-tab (+consult--tab-index)))

Additions

This approach is very extensible, here are some examples

Preselect recent tab with vertico--goto

(defvar +consult--tab-index-commands '(+tab-bar-dwim
                                       +consult-tab
                                       +consult-tab-close*)
  "List of commands that will trigger `+consult--tab-index-preselect' and `+consult--tab-index-refresh'")

(defun +consult--tab-index-preselect ()
  "Preselect recent tab if `this-command' in `+consult--tab-index-commands'."
  (when (memq this-command +consult--tab-index-commands)
    (vertico--goto (or (tab-bar--tab-index-recent 1)
                       (tab-bar--current-tab-index)))))

(add-hook 'minibuffer-setup-hook #'+consult--tab-index-preselect)

(defun +consult--tab-index-refresh ()
  "Run `consult-vertico--refresh' if `this-command' in `+consult--tab-index-commands'."
  (when (memq this-command +consult--tab-index-commands)
    (consult-vertico--refresh)))

(advice-add #'vertico--setup :after #'+consult--tab-index-refresh)

DWIM function that i took from Prot and adapted

;;;###autoload
(defun +tab-bar-dwim (&optional arg)
  "Do-What-I-Mean function for tabs.
If optional prefix argument is specified, then switch to `ARG'th tab.

If no other tab exists, create one and switch to it.

If there is one other tab (two in total), switch to it.

If there are more than two tabs, select tab with `+consult-tab'."
  (interactive "P")
  (if arg
      (tab-bar-select-tab arg)
    (pcase (length (tab-bar-tabs))
      (1 (tab-bar-new-tab))
      (2 (tab-bar-switch-to-next-tab))
      (_ (+consult-tab)))))

Close multiple tabs

;;;###autoload
(defun +consult-tab-close* ()
  "Close multiple tabs."
  (interactive)
  (let (index)
    (while (setq index (+consult--tab-index "Close tab: "))
      (tab-bar-close-tab index))))

Embark integration

(require 'embark)

(defun +embark-tab-close (tab-index)
  "Close tab."
  (tab-bar-close-tab (1- (string-to-number tab-index))))

(defun +embark-tab-rename (tab-index)
  "Rename tab."
  (setq current-prefix-arg (string-to-number tab-index))
  (call-interactively #'tab-bar-rename-tab))

(defvar-keymap +embark-tab-index-map
  :doc "Keymap for tab-index."
  "k" #'+embark-tab-close
  "r" #'+embark-tab-rename)
(add-to-list 'embark-keymap-alist '(tab-index . +embark-tab-index-map))

Emacs and web colors list.

Functions similar to counsel-colors-emacs and counsel-colors-web. Insert color name from the list of supported colors or, via embark actions, insert RGB or HEX values.

Consult colors functions

(defvar consult-colors-history nil
  "History for `consult-colors-emacs' and `consult-colors-web'.")

;; No longer preloaded in Emacs 28.
(autoload 'list-colors-duplicates "facemenu")
;; No preloaded in consult.el
(autoload 'consult--read "consult")

(defun consult-colors-emacs (color)
  "Show a list of all supported colors for a particular frame.\

You can insert the name (default), or insert or kill the hexadecimal or RGB value of the
selected color."
  (interactive
   (list (consult--read (list-colors-duplicates (defined-colors))
                        :prompt "Emacs color: "
                        :require-match t
                        :category 'color
                        :history '(:input consult-colors-history)
                        )))
  (insert color))

;; Adapted from counsel.el to get web colors.
(defun counsel-colors--web-list nil
  "Return list of CSS colors for `counsult-colors-web'."
  (require 'shr-color)
  (sort (mapcar #'downcase (mapcar #'car shr-color-html-colors-alist)) #'string-lessp))

(defun consult-colors-web (color)
  "Show a list of all CSS colors.\

You can insert the name (default), or insert or kill the hexadecimal or RGB value of the
selected color."
  (interactive
   (list (consult--read (counsel-colors--web-list)
                        :prompt "Color: "
                        :require-match t
                        :category 'color
                        :history '(:input consult-colors-history)
                        )))
  (insert color))

Embark integration

Convert color’s names

(defun rounding-numbers (list-of-num decimal-points)
  "Return (as a float) the list of nearest integers to each number of list-of-num."
  (let ((rounding (expt 10 decimal-points)))
    (mapcar (lambda (x) (/ (fround (* rounding x)) rounding)) list-of-num)))

(defun numbers-to-string (list-of-num SEPARATOR)
  "Converts a list of numbers to a string \"num1,num2,num3,...\"."
  (mapconcat #'number-to-string list-of-num SEPARATOR))

;; Colors RGB number as string
(defvar color-rgb-round-decimal-points 2 "Number of decimal points to round RGB colors.")
(defvar color-rgb-string-separator "," "SEPARATOR between numbers for RGB strings.")

(defun color-name-to-rgb-string (NAME)
  "Return the RGB value of color NAME as string \"num1,num2,num3\", with num between 0 and 1.
Return nil if NAME does not designate a valid color."
  (when-let ((rgb (color-name-to-rgb NAME)))
    (numbers-to-string rgb color-rgb-string-separator)))

(defun color-name-to-round-rgb-string (NAME)
  "Returns the rounded RGB value of color as string \"num1,num2,num3\", with num between 0 and 1.
Return nil if NAME does not designate a valid color."
  (when-let ((rgb (color-name-to-rgb NAME)))
    (numbers-to-string (rounding-numbers rgb color-rgb-round-decimal-points)
                       color-rgb-string-separator)))

;; Adapted from counsel.el to conver color name to hex.
(defun counsel-colors--hex (NAME)
  "Return hexadecimal value of color with NAME.
Return nil if NAME does not designate a valid color."
  (when-let* ((rgb (color-name-to-rgb NAME))
              ;; Sets 2 digits per component.
              (hex (apply #'color-rgb-to-hex (append rgb '(2)))))
    hex))

Embark config

Config using Doom emacs. For doomed users not using Doom, evaluate code in (after! embark ... ) using define-key instead of map!.

(after! embark
  (defvar-keymap embark-consult-color-action-map
    :doc "Keymap for embark actions in the `color' category of marginalia.")

  ;; Kill and insert versions
  (defvar embark-consult-color-functions-alist
    '(((color-name-to-round-rgb-string . "rRGB") . ("r" . "k"))
      ((color-name-to-rgb-string       . "RGB")  . ("R" . "K"))
      ((counsel-colors--hex            . "hex")  . ("h" . "H")))
    "Cons list of ((fun . desc) . (bind_insert . bind_kill)) of functions converting a color name to some value.
Used to define their `insert' and `kill-new' versions for embark actions.")

  ;; Define `insert' versions
  (cl-loop for fun in embark-consult-color-functions-alist do
           ;; (message "dir %s, name %s" (car dirname) (cdr dirname))
           (let* ((sym (caar fun))
                  (bind (cadr fun))
                  (desc (format "Insert %s" (cdar fun)))
                  (newname (intern (format "%s-insert" (symbol-name sym)))))
             ;; `(lambda (color) (insert (apply ',fun (list color))))
             (fset newname `(lambda (color)
                              (insert (,sym color))
                              (pushnew! consult-colors-history color)))
             ;; (define-key embark-consult-color-action-map (kbd bind) (cons desc newname))
             (map! :map embark-consult-color-action-map
                   :desc desc bind newname)))

  ;; Define `kill-new' versions
  (cl-loop for fun in embark-consult-color-functions-alist do
           (let* ((sym (caar fun))
                  (bind (cddr fun))
                  (desc (format "Insert %s" (cdar fun)))
                  (newname (intern (format "%s-kill" (symbol-name sym)))))
             ;; `(lambda (color) (kill-new (apply ',fun (list color))))
             (fset newname `(lambda (color)
                              (kill-new (,sym color))
                              (pushnew! consult-colors-history color)))
             ;; (define-key embark-consult-color-action-map (kbd bind) (cons desc newname))
             (map! :map embark-consult-color-action-map
                   :desc desc bind newname)))

  (add-to-list 'embark-keymap-alist '(color . embark-consult-color-action-map)))

Marginalized

Compatibility with Marginalized comes for free, since it already defines the category color, for which uses marginalia-annotate-color.

Clone this wiki locally