(eval-when-compile
(require 'cl-lib)
(require 'subr-x))
(require 'compat)
(require 'bookmark)
(defgroup consult nil
"Consulting `completing-read'."
:link '(info-link :tag "Info Manual" "(consult)")
:link '(url-link :tag "Homepage" "https://github.com/minad/consult")
:link '(emacs-library-link :tag "Library Source" "consult.el")
:group 'files
:group 'outlines
:group 'minibuffer
:prefix "consult-")
(defcustom consult-narrow-key nil
"Prefix key for narrowing during completion.
Good choices for this key are \"<\" and \"C-+\" for example. The
key must be a string accepted by `key-valid-p'."
:type '(choice string (const nil)))
(defcustom consult-widen-key nil
"Key used for widening during completion.
If this key is unset, defaults to twice the `consult-narrow-key'.
The key must be a string accepted by `key-valid-p'."
:type '(choice string (const nil)))
(defcustom consult-project-function
#'consult--default-project-function
"Function which returns project root directory.
The function takes one boolargument MAY-PROMPT. If MAY-PROMPT is non-nil,
the function may ask the prompt the user for a project directory.
The root directory is used by `consult-buffer' and `consult-grep'."
:type '(choice function (const nil)))
(defcustom consult-async-refresh-delay 0.2
"Refreshing delay of the completion ui for asynchronous commands.
The completion ui is only updated every `consult-async-refresh-delay'
seconds. This applies to asynchronous commands like for example
`consult-grep'."
:type 'float)
(defcustom consult-async-input-throttle 0.4
"Input throttle for asynchronous commands.
The asynchronous process is started only every
`consult-async-input-throttle' seconds. This applies to asynchronous
commands, e.g., `consult-grep'."
:type 'float)
(defcustom consult-async-input-debounce 0.2
"Input debounce for asynchronous commands.
The asynchronous process is started only when there has not been new
input for `consult-async-input-debounce' seconds. This applies to
asynchronous commands, e.g., `consult-grep'."
:type 'float)
(defcustom consult-async-min-input 3
"Minimum number of letters needed, before asynchronous process is called.
This applies to asynchronous commands, e.g., `consult-grep'."
:type 'integer)
(defcustom consult-async-split-style 'perl
"Async splitting style, see `consult-async-split-styles-alist'."
:type '(choice (const :tag "No splitting" nil)
(const :tag "Comma" comma)
(const :tag "Semicolon" semicolon)
(const :tag "Perl" perl)))
(defcustom consult-async-split-styles-alist
'((nil :function consult--split-nil)
(comma :separator ?, :function consult--split-separator)
(semicolon :separator ?\ (perl :initial "#" :function consult--split-perl))
"Async splitting styles."
:type '(alist :key-type symbol :value-type plist))
(defcustom consult-mode-histories
'((eshell-mode eshell-history-ring eshell-history-index eshell-bol)
(comint-mode comint-input-ring comint-input-ring-index comint-bol)
(term-mode term-input-ring term-input-ring-index term-bol))
"Alist of mode histories (mode history index bol).
The histories can be rings or lists. Index, if provided, is a
variable to set to the index of the selection within the ring or
list. Bol, if provided is a function which jumps to the beginning
of the line after the prompt."
:type '(alist :key-type symbol
:value-type (group :tag "Include Index"
(symbol :tag "List/Ring")
(symbol :tag "Index Variable")
(symbol :tag "Bol Function"))))
(defcustom consult-themes nil
"List of themes (symbols or regexps) to be presented for selection.
nil shows all `custom-available-themes'."
:type '(repeat (choice symbol regexp)))
(defcustom consult-after-jump-hook '(recenter)
"Function called after jumping to a location.
Commonly used functions for this hook are `recenter' and
`reposition-window'. You may want to add a function which pulses the
current line, e.g., `pulse-momentary-highlight-one-line' is supported on
Emacs 28 and newer. The hook called during preview and for the jump
after selection."
:type 'hook)
(defcustom consult-line-start-from-top nil
"Start search from the top if non-nil.
Otherwise start the search at the current line and wrap around."
:type 'boolean)
(defcustom consult-point-placement 'match-beginning
"Where to leave point when jumping to a match.
This setting affects the command `consult-line' and the `consult-grep' variants."
:type '(choice (const :tag "Beginning of the line" line-beginning)
(const :tag "Beginning of the match" match-beginning)
(const :tag "End of the match" match-end)))
(defcustom consult-line-numbers-widen t
"Show absolute line numbers when narrowing is active.
See also `display-line-numbers-widen'."
:type 'boolean)
(defcustom consult-goto-line-numbers t
"Show line numbers for `consult-goto-line'."
:type 'boolean)
(defcustom consult-fontify-preserve t
"Preserve fontification for line-based commands."
:type 'boolean)
(defcustom consult-fontify-max-size 1048576
"Buffers larger than this byte limit are not fontified.
This is necessary in order to prevent a large startup time
for navigation commands like `consult-line'."
:type 'integer)
(defcustom consult-buffer-filter
'("\\` "
"\\`\\*Completions\\*\\'"
"\\`\\*Flymake log\\*\\'"
"\\`\\*Semantic SymRef\\*\\'"
"\\`\\*tramp/.*\\*\\'")
"Filter regexps for `consult-buffer'.
The default setting is to filter ephemeral buffer names beginning with a space
character, the *Completions* buffer and a few log buffers."
:type '(repeat regexp))
(defcustom consult-buffer-sources
'(consult--source-hidden-buffer
consult--source-modified-buffer
consult--source-buffer
consult--source-recent-file
consult--source-file-register
consult--source-bookmark
consult--source-project-buffer
consult--source-project-recent-file)
"Sources used by `consult-buffer'.
See also `consult-project-buffer-sources'.
See `consult--multi' for a description of the source data structure."
:type '(repeat symbol))
(defcustom consult-project-buffer-sources nil
"Sources used by `consult-project-buffer'.
See also `consult-buffer-sources'.
See `consult--multi' for a description of the source data structure."
:type '(repeat symbol))
(defcustom consult-mode-command-filter
'( "-mode\\'" "--"
simple mwheel time so-long recentf)
"Filter commands for `consult-mode-command'."
:type '(repeat (choice symbol regexp)))
(defcustom consult-grep-max-columns 300
"Maximal number of columns of grep output."
:type 'integer)
(defconst consult--grep-match-regexp
"\\`\\(?:\\./\\)?\\([^\n\0]+\\)\0\\([0-9]+\\)\\([-:\0]\\)"
"Regexp used to match file and line of grep output.")
(defcustom consult-grep-args
'("grep" (consult--grep-exclude-args)
"--null --line-buffered --color=never --ignore-case\
--with-filename --line-number -I -r")
"Command line arguments for grep, see `consult-grep'.
The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
:type '(choice string (repeat (choice string expression))))
(defcustom consult-git-grep-args
"git --no-pager grep --null --color=never --ignore-case\
--extended-regexp --line-number -I"
"Command line arguments for git-grep, see `consult-git-grep'.
The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
:type '(choice string (repeat (choice string expression))))
(defcustom consult-ripgrep-args
"rg --null --line-buffered --color=never --max-columns=1000 --path-separator /\
--smart-case --no-heading --with-filename --line-number --search-zip"
"Command line arguments for ripgrep, see `consult-ripgrep'.
The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
:type '(choice string (repeat (choice string expression))))
(defcustom consult-find-args
"find . -not ( -wholename */.* -prune )"
"Command line arguments for find, see `consult-find'.
The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
:type '(choice string (repeat (choice string expression))))
(defcustom consult-locate-args
"locate --ignore-case" "Command line arguments for locate, see `consult-locate'.
The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
:type '(choice string (repeat (choice string expression))))
(defcustom consult-man-args
"man -k"
"Command line arguments for man, see `consult-man'.
The dynamically computed arguments are appended.
Can be either a string, or a list of strings or expressions."
:type '(choice string (repeat (choice string expression))))
(defcustom consult-preview-key 'any
"Preview trigger keys, can be nil, `any', a single key or a list of keys.
Debouncing can be specified via the `:debounce' attribute. The
individual keys must be strings accepted by `key-valid-p'."
:type '(choice (const :tag "Any key" any)
(list :tag "Debounced"
(const :debounce)
(float :tag "Seconds" 0.1)
(const any))
(const :tag "No preview" nil)
(string :tag "Key")
(repeat :tag "List of keys" string)))
(defcustom consult-preview-max-size 10485760
"Files larger than this byte limit are not previewed."
:type 'integer)
(defcustom consult-preview-raw-size 524288
"Files larger than this byte limit are previewed in raw form."
:type 'integer)
(defcustom consult-preview-max-count 10
"Number of files to keep open at once during preview."
:type 'integer)
(defcustom consult-preview-excluded-files nil
"List of regexps matched against names of files, which are not previewed."
:type '(repeat regexp))
(defcustom consult-preview-allowed-hooks
'(global-font-lock-mode-check-buffers
save-place-find-file-hook)
"List of `find-file' hooks, which should be executed during file preview."
:type '(repeat symbol))
(defcustom consult-preview-variables
'((inhibit-message . t)
(enable-dir-local-variables . nil)
(enable-local-variables . :safe)
(non-essential . t)
(delay-mode-hooks . t))
"Variables which are bound for file preview."
:type '(alist :key-type symbol))
(defcustom consult-bookmark-narrow
`((?f "File" ,#'bookmark-default-handler)
(?h "Help" ,#'help-bookmark-jump)
(?i "Info" ,#'Info-bookmark-jump)
(?p "Picture" ,#'image-bookmark-jump)
(?d "Docview" ,#'doc-view-bookmark-jump)
(?m "Man" ,#'Man-bookmark-jump)
(?w "Woman" ,#'woman-bookmark-jump)
(?g "Gnus" ,#'gnus-summary-bookmark-jump)
(?s "Eshell" eshell-bookmark-jump)
(?e "Eww" eww-bookmark-jump)
(?v "VC Directory" vc-dir-bookmark-jump))
"Bookmark narrowing configuration.
Each element of the list must have the form (char name handler)."
:type '(repeat (list character string function)))
(defcustom consult-yank-rotate
(if (boundp 'yank-from-kill-ring-rotate)
yank-from-kill-ring-rotate
t)
"Rotate the `kill-ring' in the `consult-yank' commands."
:type 'boolean)
(defgroup consult-faces nil
"Faces used by Consult."
:group 'consult
:group 'faces)
(defface consult-preview-line
'((t :inherit consult-preview-insertion :extend t))
"Face used for line previews.")
(defface consult-highlight-match
'((t :inherit match))
"Face used to highlight matches in the completion candidates.
Used for example in `consult-grep'.")
(defface consult-preview-match
'((t :inherit isearch))
"Face used for match previews, e.g., in `consult-line'.")
(defface consult-preview-cursor
'((t :inherit cursor))
"Face used for cursor previews and marks, e.g., in `consult-mark'.")
(defface consult-preview-insertion
'((t :inherit region))
"Face used for previews of text to be inserted.
Used by `consult-completion-in-region', `consult-yank' and `consult-history'.")
(defface consult-narrow-indicator
'((t :inherit warning))
"Face used for the narrowing indicator.")
(defface consult-async-running
'((t :inherit consult-narrow-indicator))
"Face used if asynchronous process is running.")
(defface consult-async-finished
'((t :inherit success))
"Face used if asynchronous process has finished.")
(defface consult-async-failed
'((t :inherit error))
"Face used if asynchronous process has failed.")
(defface consult-async-split
'((t :inherit font-lock-negation-char-face))
"Face used to highlight punctuation character.")
(defface consult-help
'((t :inherit shadow))
"Face used to highlight help, e.g., in `consult-register-store'.")
(defface consult-key
'((t :inherit font-lock-keyword-face))
"Face used to highlight keys, e.g., in `consult-register'.")
(defface consult-line-number
'((t :inherit consult-key))
"Face used to highlight location line in `consult-global-mark'.")
(defface consult-file
'((t :inherit font-lock-function-name-face))
"Face used to highlight files in `consult-buffer'.")
(defface consult-grep-context
'((t :inherit shadow))
"Face used to highlight grep context in `consult-grep'.")
(defface consult-bookmark
'((t :inherit font-lock-constant-face))
"Face used to highlight bookmarks in `consult-buffer'.")
(defface consult-buffer
'((t))
"Face used to highlight buffers in `consult-buffer'.")
(defface consult-line-number-prefix
'((t :inherit line-number))
"Face used to highlight line number prefixes.")
(defface consult-line-number-wrapped
'((t :inherit consult-line-number-prefix :inherit font-lock-warning-face))
"Face used to highlight line number prefixes after wrap around.")
(defface consult-separator
'((((class color) (min-colors 88) (background light))
:foreground "#ccc")
(((class color) (min-colors 88) (background dark))
:foreground "#333"))
"Face used for thin line separators in `consult-register-window'.")
(defvar consult--keep-lines-history nil)
(defvar consult--grep-history nil)
(defvar consult--find-history nil)
(defvar consult--man-history nil)
(defvar consult--line-history nil)
(defvar consult--line-multi-history nil)
(defvar consult--theme-history nil)
(defvar consult--minor-mode-menu-history nil)
(defvar consult--buffer-history nil)
(defvar consult--regexp-compiler
#'consult--default-regexp-compiler
"Regular expression compiler used by `consult-grep' and other commands.
The function must return a list of regular expressions and a highlighter
function.")
(defvar consult--customize-alist
`((,#'consult-buffer-other-frame :preview-key nil))
"Command configuration alist for fine-grained configuration.
Each element of the list must have the form (command-name plist...). The
options set here will be evaluated and passed to `consult--read', when
called from the corresponding command. Note that the options depend on
the private `consult--read' API and should not be considered as stable
as the public API.")
(defvar consult--buffer-display #'switch-to-buffer
"Buffer display function.")
(defvar consult--completion-candidate-hook
(list #'consult--default-completion-minibuffer-candidate
#'consult--default-completion-list-candidate)
"Get candidate from completion system.")
(defvar consult--completion-refresh-hook nil
"Refresh completion system.")
(defvar-local consult--preview-function nil
"Minibuffer-local variable which exposes the current preview function.
This function can be called by custom completion systems from
outside the minibuffer.")
(defvar consult--annotate-align-step 10
"Round candidate width.")
(defvar consult--annotate-align-width 0
"Maximum candidate width used for annotation alignment.")
(defconst consult--tofu-char #x200000
"Special character used to encode line prefixes for disambiguation.
We use invalid characters outside the Unicode range.")
(defconst consult--tofu-range #x100000
"Special character range.")
(defvar-local consult--narrow nil
"Current narrowing key.")
(defvar-local consult--narrow-keys nil
"Narrowing prefixes of the current completion.")
(defvar-local consult--narrow-predicate nil
"Narrowing predicate of the current completion.")
(defvar-local consult--narrow-overlay nil
"Narrowing indicator overlay.")
(defvar consult--gc-threshold (* 64 1024 1024)
"Large gc threshold for temporary increase.")
(defvar consult--gc-percentage 0.5
"Large gc percentage for temporary increase.")
(defvar consult--process-chunk (* 1024 1024)
"Increase process output chunk size.")
(defvar consult--async-log
" *consult-async*"
"Buffer for async logging output used by `consult--async-process'.")
(defvar-local consult--focus-lines-overlays nil
"Overlays used by `consult-focus-lines'.")
(defvar-local consult--org-fold-regions nil
"Stored regions for the org-fold API.")
(defun consult--in-buffer (fun &optional buffer)
"Ensure that FUN is executed inside BUFFER."
(unless buffer (setq buffer (current-buffer)))
(lambda (&rest args)
(with-current-buffer buffer
(apply fun args))))
(defun consult--completion-table-in-buffer (table &optional buffer)
"Ensure that completion TABLE is executed inside BUFFER."
(if (functionp table)
(consult--in-buffer
(lambda (str pred action)
(if (eq action 'metadata)
(mapcar
(lambda (x)
(if (and (string-suffix-p (symbol-name (car-safe x)) "-function") (cdr x))
(cons (car x) (consult--in-buffer (cdr x)))
x))
(funcall table str pred action))
(funcall table str pred action)))
buffer)
table))
(defun consult--build-args (arg)
"Return ARG as a flat list of split strings.
Turn ARG into a list, and for each element either:
- split it if it a string.
- eval it if it is an expression."
(mapcan (lambda (x)
(if (stringp x)
(split-string-and-unquote x)
(ensure-list (eval x 'lexical))))
(ensure-list arg)))
(defun consult--command-split (str)
"Return command argument and options list given input STR."
(save-match-data
(let ((opts (when (string-match " +--\\( +\\|\\'\\)" str)
(prog1 (substring str (match-end 0))
(setq str (substring str 0 (match-beginning 0)))))))
(cons str (and opts (ignore-errors (split-string-and-unquote opts)))))))
(defmacro consult--keep! (list form)
"Evaluate FORM for every element of LIST and keep the non-nil results."
(declare (indent 1))
(cl-with-gensyms (head prev result)
`(let* ((,head (cons nil ,list))
(,prev ,head))
(while (cdr ,prev)
(if-let (,result (let ((it (cadr ,prev))) ,form))
(progn
(pop ,prev)
(setcar ,prev ,result))
(setcdr ,prev (cddr ,prev))))
(setq ,list (cdr ,head))
nil)))
(defmacro consult--minibuffer-with-setup-hook (fun &rest body)
"Variant of `minibuffer-with-setup-hook' using a symbol and `fset'.
This macro is only needed to prevent memory leaking issues with
the upstream `minibuffer-with-setup-hook' macro.
FUN is the hook function and BODY opens the minibuffer."
(declare (indent 1) (debug t))
(let ((hook (gensym "hook"))
(append))
(when (eq (car-safe fun) :append)
(setq append '(t) fun (cadr fun)))
`(let ((,hook (make-symbol "consult--minibuffer-setup-hook")))
(fset ,hook (lambda ()
(remove-hook 'minibuffer-setup-hook ,hook)
(funcall ,fun)))
(unwind-protect
(progn
(add-hook 'minibuffer-setup-hook ,hook ,@append)
,@body)
(remove-hook 'minibuffer-setup-hook ,hook)))))
(defun consult--completion-filter (pattern cands category _highlight)
"Filter CANDS with PATTERN.
CATEGORY is the completion category, used to find the completion style via
`completion-category-defaults' and `completion-category-overrides'.
HIGHLIGHT must be non-nil if the resulting strings should be highlighted."
(nconc (completion-all-completions pattern cands nil (length pattern)
`(metadata (category . ,category)))
nil))
(defun consult--completion-filter-complement (pattern cands category _highlight)
"Filter CANDS with complement of PATTERN.
See `consult--completion-filter' for the arguments CATEGORY and HIGHLIGHT."
(let ((ht (consult--string-hash (consult--completion-filter pattern cands category nil))))
(seq-remove (lambda (x) (gethash x ht)) cands)))
(defun consult--completion-filter-dispatch (pattern cands category highlight)
"Filter CANDS with PATTERN with optional complement.
Either using `consult--completion-filter' or
`consult--completion-filter-complement', depending on if the pattern starts
with a bang. See `consult--completion-filter' for the arguments CATEGORY and
HIGHLIGHT."
(cond
((string-match-p "\\`!? ?\\'" pattern) cands) ((string-prefix-p "! " pattern) (consult--completion-filter-complement
(substring pattern 2) cands category nil))
(t (consult--completion-filter pattern cands category highlight))))
(defmacro consult--each-line (beg end &rest body)
"Iterate over each line.
The line beginning/ending BEG/END is bound in BODY."
(declare (indent 2))
(cl-with-gensyms (max)
`(save-excursion
(let ((,beg (point-min)) (,max (point-max)) end)
(while (< ,beg ,max)
(goto-char ,beg)
(setq ,end (pos-eol))
,@body
(setq ,beg (1+ ,end)))))))
(defun consult--display-width (string)
"Compute width of STRING taking display and invisible properties into account."
(let ((pos 0) (width 0) (end (length string)))
(while (< pos end)
(let ((nextd (next-single-property-change pos 'display string end))
(display (get-text-property pos 'display string)))
(if (stringp display)
(setq width (+ width (string-width display))
pos nextd)
(while (< pos nextd)
(let ((nexti (next-single-property-change pos 'invisible string nextd)))
(unless (get-text-property pos 'invisible string)
(setq width (+ width (compat-call string-width string pos nexti))))
(setq pos nexti))))))
width))
(defun consult--string-hash (strings)
"Create hashtable from STRINGS."
(let ((ht (make-hash-table :test #'equal :size (length strings))))
(dolist (str strings)
(puthash str t ht))
ht))
(defmacro consult--local-let (binds &rest body)
"Buffer local let BINDS of dynamic variables in BODY."
(declare (indent 1))
(let ((buffer (gensym "buffer"))
(local (mapcar (lambda (x) (cons (gensym "local") (car x))) binds)))
`(let ((,buffer (current-buffer))
,@(mapcar (lambda (x) `(,(car x) (local-variable-p ',(cdr x)))) local))
(unwind-protect
(progn
,@(mapcar (lambda (x) `(make-local-variable ',(car x))) binds)
(let (,@binds)
,@body))
(when (buffer-live-p ,buffer)
(with-current-buffer ,buffer
,@(mapcar (lambda (x)
`(unless ,(car x)
(kill-local-variable ',(cdr x))))
local)))))))
(defun consult--abbreviate-file (file)
"Return abbreviated file name of FILE for use in `completing-read' prompt."
(save-match-data
(let ((afile (abbreviate-file-name file)))
(if (string-match "/\\([^/]+\\)/\\([^/]+/?\\)\\'" afile)
(propertize (format "…/%s/%s" (match-string 1 afile) (match-string 2 afile))
'help-echo afile)
afile))))
(defun consult--directory-prompt (prompt dir)
"Return prompt, paths and default directory.
PROMPT is the prompt prefix. The directory is appended to the
prompt prefix. For projects only the project name is shown. The
`default-directory' is not shown. Other directories are
abbreviated and only the last two path components are shown.
If DIR is a string, it is returned as default directory. If DIR
is a list of strings, the list is returned as search paths. If
DIR is nil the `consult-project-function' is tried to retrieve
the default directory. If no project is found the
`default-directory' is returned as is. Otherwise the user is
asked for the directories or files to search via
`completing-read-multiple'."
(let* ((paths nil)
(dir
(pcase dir
((pred stringp) dir)
('nil (or (consult--project-root) default-directory))
(_
(pcase (if (stringp (car-safe dir))
dir
(let ((this-command this-command))
(completing-read-multiple "Directories or files: "
#'completion-file-name-table
nil t nil 'file-name-history)))
((and `(,p) (guard (file-directory-p p))) p)
(ps (setq paths ps)
default-directory)))))
(edir (file-name-as-directory (expand-file-name dir)))
(pdir (let ((default-directory edir))
(consult--project-root))))
(list
(format "%s (%s): " prompt
(pcase paths
(`(,p) (consult--abbreviate-file p))
(`(,p . ,_)
(format "%d paths, %s, …" (length paths) (consult--abbreviate-file p)))
((guard (equal edir pdir)) (concat "Project " (consult--project-name pdir)))
(_ (consult--abbreviate-file edir))))
(or paths '("."))
edir)))
(defun consult--default-project-function (may-prompt)
"Return project root directory.
When no project is found and MAY-PROMPT is non-nil ask the user."
(when-let (proj (project-current may-prompt))
(cond
((fboundp 'project-root) (project-root proj))
((fboundp 'project-roots) (car (project-roots proj))))))
(defun consult--project-root (&optional may-prompt)
"Return project root as absolute path.
When no project is found and MAY-PROMPT is non-nil ask the user."
(let ((this-command this-command))
(when-let (root (and consult-project-function
(funcall consult-project-function may-prompt)))
(expand-file-name root))))
(defun consult--project-name (dir)
"Return the project name for DIR."
(if (string-match "/\\([^/]+\\)/\\'" dir)
(propertize (match-string 1 dir) 'help-echo (abbreviate-file-name dir))
dir))
(defun consult--format-file-line-match (file line match)
"Format string FILE:LINE:MATCH with faces."
(setq line (number-to-string line)
match (concat file ":" line ":" match)
file (length file))
(put-text-property 0 file 'face 'consult-file match)
(put-text-property (1+ file) (+ 1 file (length line)) 'face 'consult-line-number match)
match)
(defun consult--make-overlay (beg end &rest props)
"Make consult overlay between BEG and END with PROPS."
(let ((ov (make-overlay beg end)))
(while props
(overlay-put ov (car props) (cadr props))
(setq props (cddr props)))
ov))
(defun consult--remove-dups (list)
"Remove duplicate strings from LIST."
(delete-dups (copy-sequence list)))
(defsubst consult--in-range-p (pos)
"Return t if position POS lies in range `point-min' to `point-max'."
(<= (point-min) pos (point-max)))
(defun consult--completion-window-p ()
"Return non-nil if the selected window belongs to the completion UI."
(or (eq (selected-window) (active-minibuffer-window))
(eq #'completion-list-mode (buffer-local-value 'major-mode (window-buffer)))))
(defun consult--forbid-minibuffer ()
"Raise an error if executed from the minibuffer."
(when (minibufferp)
(user-error "`%s' called inside the minibuffer" this-command)))
(defun consult--require-minibuffer ()
"Raise an error if executed outside the minibuffer."
(unless (minibufferp)
(user-error "`%s' must be called inside the minibuffer" this-command)))
(defun consult--fontify-all ()
"Ensure that the whole buffer is fontified."
(when (and consult-fontify-preserve jit-lock-mode
(< (buffer-size) consult-fontify-max-size))
(jit-lock-fontify-now)))
(defun consult--fontify-region (start end)
"Ensure that region between START and END is fontified."
(when (and consult-fontify-preserve jit-lock-mode)
(jit-lock-fontify-now start end)))
(defmacro consult--with-increased-gc (&rest body)
"Temporarily increase the gc limit in BODY to optimize for throughput."
(cl-with-gensyms (overwrite)
`(let* ((,overwrite (> consult--gc-threshold gc-cons-threshold))
(gc-cons-threshold (if ,overwrite consult--gc-threshold gc-cons-threshold))
(gc-cons-percentage (if ,overwrite consult--gc-percentage gc-cons-percentage)))
,@body)))
(defmacro consult--slow-operation (message &rest body)
"Show delayed MESSAGE if BODY takes too long.
Also temporarily increase the gc limit via `consult--with-increased-gc'."
(declare (indent 1))
`(with-delayed-message (1 ,message)
(consult--with-increased-gc
,@body)))
(defun consult--count-lines (pos)
"Move to position POS and return number of lines."
(let ((line 1))
(while (< (point) pos)
(forward-line)
(when (<= (point) pos)
(cl-incf line)))
(goto-char pos)
line))
(defun consult--marker-from-line-column (buffer line column)
"Get marker in BUFFER from LINE and COLUMN."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(ignore-errors
(forward-line (1- line))
(forward-char column))
(point-marker))))))
(defun consult--line-prefix (&optional curr-line)
"Annotate `consult-location' candidates with line numbers.
CURR-LINE is the current line number."
(setq curr-line (or curr-line -1))
(let* ((width (length (number-to-string (line-number-at-pos
(point-max)
consult-line-numbers-widen))))
(before (format #("%%%dd " 0 6 (face consult-line-number-wrapped)) width))
(after (format #("%%%dd " 0 6 (face consult-line-number-prefix)) width)))
(lambda (cand)
(let ((line (cdr (get-text-property 0 'consult-location cand))))
(list cand (format (if (< line curr-line) before after) line) "")))))
(defsubst consult--location-candidate (cand marker line tofu &rest props)
"Add MARKER and LINE as `consult-location' text property to CAND.
Furthermore add the additional text properties PROPS, and append
TOFU suffix for disambiguation."
(setq cand (concat cand (consult--tofu-encode tofu)))
(add-text-properties 0 1 `(consult-location (,marker . ,line) ,@props) cand)
cand)
(defconst consult--remove-text-properties
'(category cursor cursor-intangible cursor-sensor-functions field follow-link
fontified front-sticky help-echo insert-behind-hooks insert-in-front-hooks
intangible keymap local-map modification-hooks mouse-face pointer read-only
rear-nonsticky yank-handler)
"List of text properties to remove from buffer strings.")
(defsubst consult--buffer-substring (beg end &optional fontify)
"Return buffer substring between BEG and END.
If FONTIFY and `consult-fontify-preserve' are non-nil, first ensure that the
region has been fontified."
(if consult-fontify-preserve
(let (str)
(when fontify (consult--fontify-region beg end))
(setq str (buffer-substring beg end))
(remove-list-of-text-properties
0 (- end beg) consult--remove-text-properties str)
str)
(buffer-substring-no-properties beg end)))
(defun consult--region-with-cursor (beg end marker)
"Return region string with a marking at the cursor position.
BEG is the begin position.
END is the end position.
MARKER is the cursor position."
(let ((str (consult--buffer-substring beg end 'fontify)))
(if (>= marker end)
(concat str #(" " 0 1 (face consult-preview-cursor)))
(put-text-property (- marker beg) (- (1+ marker) beg)
'face 'consult-preview-cursor str)
str)))
(defun consult--line-with-cursor (marker)
"Return current line where the cursor MARKER is highlighted."
(consult--region-with-cursor (pos-bol) (pos-eol) marker))
(defsubst consult--tofu-p (char)
"Return non-nil if CHAR is a tofu."
(<= consult--tofu-char char (+ consult--tofu-char consult--tofu-range -1)))
(defun consult--tofu-hide (str)
"Hide the tofus in STR."
(let* ((max (length str))
(end max))
(while (and (> end 0) (consult--tofu-p (aref str (1- end))))
(cl-decf end))
(when (< end max)
(setq str (copy-sequence str))
(put-text-property end max 'invisible t str))
str))
(defsubst consult--tofu-append (cand id)
"Append tofu-encoded ID to CAND.
The ID must fit within a single character. It must be smaller
than `consult--tofu-range'."
(setq id (char-to-string (+ consult--tofu-char id)))
(add-text-properties 0 1 '(invisible t consult-strip t) id)
(concat cand id))
(defsubst consult--tofu-get (cand)
"Extract tofu-encoded ID from CAND.
See `consult--tofu-append'."
(- (aref cand (1- (length cand))) consult--tofu-char))
(defun consult--tofu-encode (n)
"Return tofu-encoded number N as a string.
Large numbers are encoded as multiple tofu characters."
(let (str tofu)
(while (progn
(setq tofu (char-to-string
(+ consult--tofu-char (% n consult--tofu-range)))
str (if str (concat tofu str) tofu))
(and (>= n consult--tofu-range)
(setq n (/ n consult--tofu-range)))))
(add-text-properties 0 (length str) '(invisible t consult-strip t) str)
str))
(defun consult--find-highlights (str start &rest ignored-faces)
"Find highlighted regions in STR from position START.
Highlighted regions have a non-nil face property.
IGNORED-FACES are ignored when searching for matches."
(let (highlights
(end (length str))
(beg start))
(while (< beg end)
(let ((next (next-single-property-change beg 'face str end))
(val (get-text-property beg 'face str)))
(when (and val
(not (memq val ignored-faces))
(not (and (consp val)
(seq-some (lambda (x) (memq x ignored-faces)) val))))
(push (cons (- beg start) (- next start)) highlights))
(setq beg next)))
(nreverse highlights)))
(defun consult--point-placement (str start &rest ignored-faces)
"Compute point placement from STR with START offset.
IGNORED-FACES are ignored when searching for matches.
Return cons of point position and a list of match begin/end pairs."
(let* ((matches (apply #'consult--find-highlights str start ignored-faces))
(pos (pcase-exhaustive consult-point-placement
('match-beginning (or (caar matches) 0))
('match-end (or (cdar (last matches)) 0))
('line-beginning 0))))
(dolist (match matches)
(cl-decf (car match) pos)
(cl-decf (cdr match) pos))
(cons pos matches)))
(defun consult--highlight-regexps (regexps ignore-case str)
"Highlight REGEXPS in STR.
If a regular expression contains capturing groups, only these are highlighted.
If no capturing groups are used highlight the whole match. Case is ignored
if IGNORE-CASE is non-nil."
(dolist (re regexps)
(let ((i 0))
(while (and (let ((case-fold-search ignore-case))
(string-match re str i))
(> (match-end 0) i))
(let ((m (match-data)))
(setq i (cadr m) m (or (cddr m) m))
(while m
(when (car m)
(add-face-text-property (car m) (cadr m)
'consult-highlight-match nil str))
(setq m (cddr m)))))))
str)
(defconst consult--convert-regexp-table
(append
'(("\\<" . "\\b") ("\\>" . "\\b")
("\\_<" . "\\b") ("\\_>" . "\\b"))
'(("\\`" . "^") ("\\'" . "$"))
(mapcan (lambda (x)
(mapcar (lambda (y)
(cons (concat x y)
(concat (string-remove-prefix "\\" x) "\\" y)))
'("*" "+" "?")))
'("" "\\(" "\\(?:" "\\|" "^"))
(mapcan (lambda (x) `(,x (,(cdr x) . ,(car x))))
'(("\\|" . "|")
("\\(" . "(") ("\\)" . ")")
("\\{" . "{") ("\\}" . "}"))))
"Regexp conversion table.")
(defun consult--convert-regexp (regexp type)
"Convert Emacs REGEXP to regexp syntax TYPE."
(if (memq type '(emacs basic))
regexp
(replace-regexp-in-string
(rx (or "\\\\" "\\^" (seq (or "\\(?:" "\\|") (any "*+?")) (seq "\\(" (any "*+")) (seq (or bos "^") (any "*+?")) (seq (opt "\\") (any "(){|}")) (seq "\\" (any "'<>`")) (seq "\\_" (any "<>")))) (lambda (x) (or (cdr (assoc x consult--convert-regexp-table)) x))
regexp 'fixedcase 'literal)))
(defun consult--default-regexp-compiler (input type ignore-case)
"Compile the INPUT string to a list of regular expressions.
The function should return a pair, the list of regular expressions and a
highlight function. The highlight function should take a single
argument, the string to highlight given the INPUT. TYPE is the desired
type of regular expression, which can be `basic', `extended', `emacs' or
`pcre'. If IGNORE-CASE is non-nil return a highlight function which
matches case insensitively."
(setq input (consult--split-escaped input))
(cons (mapcar (lambda (x) (consult--convert-regexp x type)) input)
(when-let (regexps (seq-filter #'consult--valid-regexp-p input))
(apply-partially #'consult--highlight-regexps regexps ignore-case))))
(defun consult--split-escaped (str)
"Split STR at spaces, which can be escaped with backslash."
(mapcar
(lambda (x) (string-replace "\0" " " x))
(split-string (replace-regexp-in-string
"\\\\\\\\\\|\\\\ "
(lambda (x) (if (equal x "\\ ") "\0" x))
str 'fixedcase 'literal)
" +" t)))
(defun consult--join-regexps (regexps type)
"Join REGEXPS of TYPE."
(cond
((and (eq type 'pcre) (cdr regexps))
(concat "^" (mapconcat (lambda (x) (format "(?=.*%s)" x))
regexps "")))
((eq type 'basic)
(string-join regexps ".*"))
(t
(when (length> regexps 3)
(message "Too many regexps, %S ignored. Use post-filtering!"
(string-join (seq-drop regexps 3) " "))
(setq regexps (seq-take regexps 3)))
(consult--regexp-join-permutations regexps
(and (memq type '(basic emacs)) "\\")))))
(defun consult--regexp-join-permutations (regexps esc)
"Join all permutations of REGEXPS.
ESC is the escaping string for choice and groups."
(pcase regexps
('nil "")
(`(,r) r)
(`(,r1 ,r2) (concat r1 ".*" r2 esc "|" r2 ".*" r1))
(_ (mapconcat
(lambda (r)
(concat r ".*" esc "("
(consult--regexp-join-permutations (remove r regexps) esc)
esc ")"))
regexps (concat esc "|")))))
(defun consult--valid-regexp-p (re)
"Return t if regexp RE is valid."
(condition-case nil
(progn (string-match-p re "") t)
(invalid-regexp nil)))
(defun consult--regexp-filter (regexps)
"Create filter regexp from REGEXPS."
(if (stringp regexps)
regexps
(mapconcat (lambda (x) (concat "\\(?:" x "\\)")) regexps "\\|")))
(defun consult--lookup-member (selected candidates &rest _)
"Lookup SELECTED in CANDIDATES list, return original element."
(car (member selected candidates)))
(defun consult--lookup-cons (selected candidates &rest _)
"Lookup SELECTED in CANDIDATES alist, return cons."
(assoc selected candidates))
(defun consult--lookup-cdr (selected candidates &rest _)
"Lookup SELECTED in CANDIDATES alist, return cdr of element."
(cdr (assoc selected candidates)))
(defun consult--lookup-location (selected candidates &rest _)
"Lookup SELECTED in CANDIDATES list of `consult-location' category.
Return the location marker."
(when-let (found (member selected candidates))
(setq found (car (consult--get-location (car found))))
(and (or (not (markerp found)) (marker-buffer found)) found)))
(defun consult--lookup-prop (prop selected candidates &rest _)
"Lookup SELECTED in CANDIDATES list and return PROP value."
(when-let (found (member selected candidates))
(get-text-property 0 prop (car found))))
(defun consult--lookup-candidate (selected candidates &rest _)
"Lookup SELECTED in CANDIDATES list and return property `consult--candidate'."
(consult--lookup-prop 'consult--candidate selected candidates))
(defun consult--filter-find-file-hook (orig &rest hooks)
"Filter `find-file-hook' by `consult-preview-allowed-hooks'.
This function is an advice for `run-hooks'.
ORIG is the original function, HOOKS the arguments."
(if (memq 'find-file-hook hooks)
(cl-letf* (((default-value 'find-file-hook)
(seq-filter (lambda (x)
(memq x consult-preview-allowed-hooks))
(default-value 'find-file-hook)))
(find-file-hook (default-value 'find-file-hook)))
(apply orig hooks))
(apply orig hooks)))
(defun consult--find-file-temporarily-1 (name)
"Open file NAME, helper function for `consult--find-file-temporarily'."
(when-let (((not (seq-find (lambda (x) (string-match-p x name))
consult-preview-excluded-files)))
(attrs (ignore-errors (file-attributes name)))
(size (file-attribute-size attrs)))
(if (> size consult-preview-max-size)
(format "File `%s' (%s) is too large for preview"
name (file-size-human-readable size))
(let ((buf (find-file-noselect name 'nowarn (> size consult-preview-raw-size))))
(cond
((and (> size consult-preview-raw-size)
(with-current-buffer buf
(save-excursion
(goto-char (point-min))
(search-forward "\0" nil 'noerror))))
(kill-buffer buf)
(format "Binary file `%s' not previewed literally" name))
((ignore-errors (buffer-local-value 'so-long-detected-p buf))
(kill-buffer buf)
(format "File `%s' with long lines not previewed" name))
(t buf))))))
(defun consult--find-file-temporarily (name)
"Open file NAME temporarily for preview."
(let ((vars (delq nil
(mapcar
(pcase-lambda (`(,k . ,v))
(if (boundp k)
(list k v (default-value k) (symbol-value k))
(message "consult-preview-variables: The variable `%s' is not bound" k)
nil))
consult-preview-variables)))
buf)
(unwind-protect
(progn
(advice-add #'run-hooks :around #'consult--filter-find-file-hook)
(pcase-dolist (`(,k ,v . ,_) vars)
(set-default k v)
(set k v))
(setq buf (consult--find-file-temporarily-1 name)))
(advice-remove #'run-hooks #'consult--filter-find-file-hook)
(pcase-dolist (`(,k ,_ ,d ,v) vars)
(set-default k d)
(set k v)))
(if (stringp buf) (progn (message "%s" buf) nil) buf)))
(defun consult--temporary-files ()
"Return a function to open files temporarily for preview."
(let ((dir default-directory)
(hook (make-symbol "consult--temporary-files-upgrade-hook"))
(orig-buffers (buffer-list))
temporary-buffers)
(fset hook
(lambda (_)
(unless (consult--completion-window-p)
(let (live-files)
(pcase-dolist (`(,file . ,buf) temporary-buffers)
(when-let (wins (and (buffer-live-p buf)
(get-buffer-window-list buf)))
(push (cons file (mapcar
(lambda (win)
(cons win (window-state-get win t)))
wins))
live-files)))
(pcase-dolist (`(,_ . ,buf) temporary-buffers)
(kill-buffer buf))
(setq temporary-buffers nil)
(pcase-dolist (`(,file . ,wins) live-files)
(when-let (buf (find-file-noselect file))
(push buf orig-buffers)
(pcase-dolist (`(,win . ,state) wins)
(setf (car (alist-get 'buffer state)) buf)
(window-state-put state win))))))))
(lambda (&optional name)
(if name
(let ((default-directory dir))
(setq name (abbreviate-file-name (expand-file-name name)))
(or
(if (and (fboundp 'dired-find-buffer-nocreate) (file-directory-p name))
(dired-find-buffer-nocreate name)
(get-file-buffer name))
(cdr (assoc name temporary-buffers))
(when-let (buf (consult--find-file-temporarily name))
(unless (or (rassq buf temporary-buffers) (memq buf orig-buffers))
(add-hook 'window-selection-change-functions hook)
(push (cons name buf) temporary-buffers)
(with-current-buffer buf
(rename-buffer
(format " Preview:%s"
(file-name-nondirectory (directory-file-name name)))
'unique))
(let ((hook (make-symbol "consult--temporary-files-disassociate-hook")))
(fset hook (lambda ()
(when (buffer-live-p buf)
(with-current-buffer buf
(remove-hook 'pre-command-hook hook)
(setq-local buffer-read-only t
dired-directory nil
buffer-file-name nil)))))
(add-hook 'pre-command-hook hook))
(while (length> temporary-buffers consult-preview-max-count)
(kill-buffer (cdar (last temporary-buffers)))
(setq temporary-buffers (nbutlast temporary-buffers))))
buf)))
(remove-hook 'window-selection-change-functions hook)
(pcase-dolist (`(,_ . ,buf) temporary-buffers)
(kill-buffer buf))
(setq temporary-buffers nil)))))
(declare-function org-fold-core-region "org-fold-core")
(declare-function org-fold-core-get-regions "org-fold-core")
(defun consult--invisible-open-permanently ()
"Open overlays which hide the current line.
See `isearch-open-necessary-overlays' and `isearch-open-overlay-temporary'."
(if (and (derived-mode-p #'org-mode) (fboundp 'org-fold-show-set-visibility))
(org-fold-show-set-visibility 'canonical)
(dolist (ov (overlays-in (pos-bol) (pos-eol)))
(when-let (fun (overlay-get ov 'isearch-open-invisible))
(when (invisible-p (overlay-get ov 'invisible))
(funcall fun ov))))))
(defun consult--invisible-open-temporarily ()
"Temporarily open overlays which hide the current line.
See `isearch-open-necessary-overlays' and `isearch-open-overlay-temporary'."
(if (and (derived-mode-p #'org-mode) (fboundp 'org-fold-show-set-visibility))
(progn
(unless consult--org-fold-regions
(setq consult--org-fold-regions
(delq nil (org-fold-core-get-regions
:with-markers t :from (point-min) :to (point-max))))
(when consult--org-fold-regions
(let ((hook (make-symbol "consult--invisible-open-temporarily-cleanup-hook"))
(buffer (current-buffer))
(depth (recursion-depth)))
(fset hook
(lambda ()
(when (= (recursion-depth) depth)
(remove-hook 'minibuffer-exit-hook hook)
(run-at-time
0 nil
(lambda ()
(when (buffer-live-p buffer)
(with-current-buffer buffer
(pcase-dolist (`(,beg ,end ,_) consult--org-fold-regions)
(when (markerp beg) (set-marker beg nil))
(when (markerp end) (set-marker end nil)))
(kill-local-variable 'consult--org-fold-regions))))))))
(add-hook 'minibuffer-exit-hook hook))))
(org-fold-show-set-visibility 'canonical)
(list (lambda ()
(pcase-dolist (`(,beg ,end ,spec) consult--org-fold-regions)
(org-fold-core-region beg end t spec)))))
(let (restore)
(dolist (ov (overlays-in (pos-bol) (pos-eol)))
(let ((inv (overlay-get ov 'invisible)))
(when (and (invisible-p inv) (overlay-get ov 'isearch-open-invisible))
(push (if-let (fun (overlay-get ov 'isearch-open-invisible-temporary))
(progn
(funcall fun ov nil)
(lambda () (funcall fun ov t)))
(overlay-put ov 'invisible nil)
(lambda () (overlay-put ov 'invisible inv)))
restore))))
restore)))
(defun consult--jump-1 (pos)
"Go to POS and recenter."
(if (and (markerp pos) (not (marker-buffer pos)))
(message "Buffer is dead")
(when-let (buf (and (markerp pos) (marker-buffer pos)))
(unless (and (eq (current-buffer) buf) (eq (window-buffer) buf))
(consult--buffer-action buf 'norecord)))
(unless (= (goto-char pos) (point))
(widen)
(goto-char pos))))
(defun consult--jump (pos)
"Push current position to mark ring, go to POS and recenter."
(when pos
(when (consp pos) (setq pos (car pos)))
(when (or (not (markerp pos)) (eq (current-buffer) (marker-buffer pos)))
(setq pos (+ pos 0))
(push-mark (point) t))
(consult--jump-1 pos)
(consult--invisible-open-permanently)
(run-hooks 'consult-after-jump-hook))
nil)
(defun consult--jump-preview ()
"The preview function used if selecting from a list of candidate positions.
The function can be used as the `:state' argument of `consult--read'."
(let ((saved-min (point-min-marker))
(saved-max (point-max-marker))
(saved-pos (point-marker))
overlays invisible)
(set-marker-insertion-type saved-max t) (lambda (action cand)
(when (eq action 'preview)
(mapc #'funcall invisible)
(mapc #'delete-overlay overlays)
(setq invisible nil overlays nil)
(if (not cand)
(let ((saved-buffer (marker-buffer saved-pos)))
(if (not saved-buffer)
(message "Buffer is dead")
(set-buffer saved-buffer)
(narrow-to-region saved-min saved-max)
(goto-char saved-pos)))
(consult--jump-1 (or (car-safe cand) cand))
(setq invisible (consult--invisible-open-temporarily)
overlays
(list (save-excursion
(let ((vbeg (progn (beginning-of-visual-line) (point)))
(vend (progn (end-of-visual-line) (point)))
(end (pos-eol)))
(consult--make-overlay vbeg (if (= vend end) (1+ end) vend)
'face 'consult-preview-line
'window (selected-window)
'priority 1)))
(consult--make-overlay (point) (1+ (point))
'face 'consult-preview-cursor
'window (selected-window)
'priority 3)))
(dolist (match (cdr-safe cand))
(push (consult--make-overlay (+ (point) (car match))
(+ (point) (cdr match))
'face 'consult-preview-match
'window (selected-window)
'priority 2)
overlays))
(run-hooks 'consult-after-jump-hook))))))
(defun consult--jump-state ()
"The state function used if selecting from a list of candidate positions."
(consult--state-with-return (consult--jump-preview) #'consult--jump))
(defun consult--get-location (cand)
"Return location from CAND."
(let ((loc (get-text-property 0 'consult-location cand)))
(when (consp (car loc))
(setcar loc (set-marker (make-marker) (cdar loc) (caar loc))))
loc))
(defun consult--location-state (candidates)
"Location state function.
The cheap location markers from CANDIDATES are upgraded on window
selection change to full Emacs markers."
(let ((jump (consult--jump-state))
(hook (make-symbol "consult--location-upgrade-hook")))
(fset hook
(lambda (_)
(unless (consult--completion-window-p)
(remove-hook 'window-selection-change-functions hook)
(mapc #'consult--get-location
(if (functionp candidates) (funcall candidates) candidates)))))
(lambda (action cand)
(pcase action
('setup (add-hook 'window-selection-change-functions hook))
('exit (remove-hook 'window-selection-change-functions hook)))
(funcall jump action cand))))
(defun consult--state-with-return (state return)
"Compose STATE function with RETURN function."
(lambda (action cand)
(funcall state action cand)
(when (and cand (eq action 'return))
(funcall return cand))))
(defmacro consult--define-state (type)
"Define state function for TYPE."
`(defun ,(intern (format "consult--%s-state" type)) ()
,(format "State function for %ss with preview.
The result can be passed as :state argument to `consult--read'." type)
(consult--state-with-return (,(intern (format "consult--%s-preview" type)))
#',(intern (format "consult--%s-action" type)))))
(defun consult--preview-key-normalize (preview-key)
"Normalize PREVIEW-KEY, return alist of keys and debounce times."
(let ((keys)
(debounce 0))
(setq preview-key (ensure-list preview-key))
(while preview-key
(if (eq (car preview-key) :debounce)
(setq debounce (cadr preview-key)
preview-key (cddr preview-key))
(let ((key (car preview-key)))
(unless (eq key 'any)
(if (key-valid-p key)
(setq key (key-parse key))
(message "Invalid preview key according to `key-valid-p': %S" key)))
(push (cons key debounce) keys))
(pop preview-key)))
keys))
(defun consult--preview-key-debounce (preview-key cand)
"Return debounce value of PREVIEW-KEY given the current candidate CAND."
(when (and (consp preview-key) (memq :keys preview-key))
(setq preview-key (funcall (plist-get preview-key :predicate) cand)))
(let ((map (make-sparse-keymap))
(keys (this-single-command-keys))
any)
(pcase-dolist (`(,k . ,d) (consult--preview-key-normalize preview-key))
(if (eq k 'any)
(setq any d)
(define-key map k `(lambda () ,d))))
(setq keys (lookup-key map keys))
(if (functionp keys) (funcall keys) any)))
(defun consult--preview-append-local-pch (fun)
"Append FUN to local `post-command-hook' list."
(let ((hook (make-symbol "consult--preview-post-command-hook")))
(fset hook fun)
(setq-local post-command-hook
(append
(remove t post-command-hook)
(list hook)
(and (memq t post-command-hook) '(t))))))
(defun consult--with-preview-1 (preview-key state transform candidate fun)
"Add preview support for FUN.
See `consult--with-preview' for the arguments
PREVIEW-KEY, STATE, TRANSFORM and CANDIDATE."
(let ((mb-input "") mb-narrow selected timer previewed)
(consult--minibuffer-with-setup-hook
(if (and state preview-key)
(lambda ()
(let ((hook (make-symbol "consult--preview-minibuffer-exit-hook"))
(depth (recursion-depth)))
(fset hook
(lambda ()
(when (= (recursion-depth) depth)
(remove-hook 'minibuffer-exit-hook hook)
(when timer
(cancel-timer timer)
(setq timer nil))
(with-selected-window (or (minibuffer-selected-window) (next-window))
(when previewed
(funcall state 'preview nil))
(funcall state 'exit nil)))))
(add-hook 'minibuffer-exit-hook hook))
(with-selected-window (or (minibuffer-selected-window) (next-window))
(funcall state 'setup nil))
(setq consult--preview-function
(lambda ()
(when-let ((cand (funcall candidate)))
(setq cand (substring-no-properties cand))
(with-selected-window (active-minibuffer-window)
(let ((input (minibuffer-contents-no-properties))
(narrow consult--narrow))
(with-selected-window (or (minibuffer-selected-window) (next-window))
(when-let ((transformed (funcall transform narrow input cand))
(debounce (consult--preview-key-debounce preview-key transformed)))
(when timer
(cancel-timer timer)
(setq timer nil))
(unless (equal-including-properties previewed transformed)
(if (> debounce 0)
(let ((win (selected-window)))
(setq timer
(run-at-time
debounce nil
(lambda ()
(when (window-live-p win)
(with-selected-window win
(funcall state 'preview (setq previewed transformed))))))))
(funcall state 'preview (setq previewed transformed)))))))))))
(consult--preview-append-local-pch
(lambda ()
(setq mb-input (minibuffer-contents-no-properties)
mb-narrow consult--narrow)
(funcall consult--preview-function))))
(lambda ()
(consult--preview-append-local-pch
(lambda ()
(setq mb-input (minibuffer-contents-no-properties)
mb-narrow consult--narrow)))))
(unwind-protect
(cons (setq selected (when-let (result (funcall fun))
(funcall transform mb-narrow mb-input result)))
mb-input)
(when state
(funcall state 'return selected))))))
(defmacro consult--with-preview (preview-key state transform candidate &rest body)
"Add preview support to BODY.
STATE is the state function.
TRANSFORM is the transformation function.
CANDIDATE is the function returning the current candidate.
PREVIEW-KEY are the keys which triggers the preview.
The state function takes two arguments, an action argument and the
selected candidate. The candidate argument can be nil if no candidate is
selected or if the selection was aborted. The function is called in
sequence with the following arguments:
1. \\='setup nil After entering the mb (minibuffer-setup-hook).
⎧ 2. \\='preview CAND/nil Preview candidate CAND or reset if CAND is nil.
⎪ \\='preview CAND/nil
⎪ \\='preview CAND/nil
⎪ ...
⎩ 3. \\='preview nil Reset preview.
4. \\='exit nil Before exiting the mb (minibuffer-exit-hook).
5. \\='return CAND/nil After leaving the mb, CAND has been selected.
The state function is always executed with the original window selected,
see `minibuffer-selected-window'. The state function is called once in
the beginning of the minibuffer setup with the `setup' argument. This is
useful in order to perform certain setup operations which require that
the minibuffer is initialized. During completion candidates are
previewed. Then the function is called with the `preview' argument and a
candidate CAND or nil if no candidate is selected. Furthermore if nil is
passed for CAND, then the preview must be undone and the original state
must be restored. The call with the `exit' argument happens once at the
end of the completion process, just before exiting the minibuffer. The
minibuffer is still alive at that point. Both `setup' and `exit' are
only useful for setup and cleanup operations. They don't receive a
candidate as argument. After leaving the minibuffer, the selected
candidate or nil is passed to the state function with the action
argument `return'. At this point the state function can perform the
actual action on the candidate. The state function with the `return'
argument is the continuation of `consult--read'. Via `unwind-protect' it
is guaranteed, that if the `setup' action of a state function is
invoked, the state function will also be called with `exit' and
`return'."
(declare (indent 4))
`(consult--with-preview-1 ,preview-key ,state ,transform ,candidate (lambda () ,@body)))
(defun consult--prefix-group (cand transform)
"Return title for CAND or TRANSFORM the candidate.
The candidate must have a `consult--prefix-group' property."
(if transform
(substring cand (1+ (length (get-text-property 0 'consult--prefix-group cand))))
(get-text-property 0 'consult--prefix-group cand)))
(defun consult--type-group (types)
"Return group function for TYPES."
(lambda (cand transform)
(if transform cand
(alist-get (get-text-property 0 'consult--type cand) types))))
(defun consult--type-narrow (types)
"Return narrowing configuration from TYPES."
(list :predicate
(lambda (cand) (eq (get-text-property 0 'consult--type cand) consult--narrow))
:keys types))
(defun consult--widen-key ()
"Return widening key, if `consult-widen-key' is not set.
The default is twice the `consult-narrow-key'."
(cond
(consult-widen-key
(if (key-valid-p consult-widen-key)
(key-parse consult-widen-key)
(message "Invalid `consult-widen-key' according to `key-valid-p': %S" consult-widen-key)
consult-widen-key))
(consult-narrow-key
(let ((key consult-narrow-key))
(if (key-valid-p key)
(setq key (key-parse key))
(message "Invalid `consult-narrow-key' according to `key-valid-p': %S" key))
(vconcat key key)))))
(defun consult-narrow (key)
"Narrow current completion with KEY.
This command is used internally by the narrowing system of `consult--read'."
(interactive
(list (unless (equal (this-single-command-keys) (consult--widen-key))
last-command-event)))
(consult--require-minibuffer)
(setq consult--narrow key)
(when consult--narrow-predicate
(setq minibuffer-completion-predicate (and consult--narrow consult--narrow-predicate)))
(when consult--narrow-overlay
(delete-overlay consult--narrow-overlay))
(when consult--narrow
(setq consult--narrow-overlay
(consult--make-overlay
(1- (minibuffer-prompt-end)) (minibuffer-prompt-end)
'before-string
(propertize (format " [%s]" (alist-get consult--narrow
consult--narrow-keys))
'face 'consult-narrow-indicator))))
(run-hooks 'consult--completion-refresh-hook))
(defconst consult--narrow-delete
`(menu-item
"" nil :filter
,(lambda (&optional _)
(when (equal (minibuffer-contents-no-properties) "")
(lambda ()
(interactive)
(consult-narrow nil))))))
(defconst consult--narrow-space
`(menu-item
"" nil :filter
,(lambda (&optional _)
(let ((str (minibuffer-contents-no-properties)))
(when-let (pair (or (and (length= str 1)
(assoc (aref str 0) consult--narrow-keys))
(and (equal str "")
(assoc 32 consult--narrow-keys))))
(lambda ()
(interactive)
(delete-minibuffer-contents)
(consult-narrow (car pair))))))))
(defun consult-narrow-help ()
"Print narrowing help as a `minibuffer-message'.
This command can be bound to a key in `consult-narrow-map',
to make it available for commands with narrowing."
(interactive)
(consult--require-minibuffer)
(let ((minibuffer-message-timeout 1000000))
(minibuffer-message
(mapconcat (lambda (x)
(concat
(propertize (key-description (list (car x))) 'face 'consult-key)
" "
(propertize (cdr x) 'face 'consult-help)))
consult--narrow-keys
" "))))
(defun consult--narrow-setup (settings map)
"Setup narrowing with SETTINGS and keymap MAP."
(if (memq :keys settings)
(setq consult--narrow-predicate (plist-get settings :predicate)
consult--narrow-keys (plist-get settings :keys))
(setq consult--narrow-predicate nil
consult--narrow-keys settings))
(when-let ((key consult-narrow-key))
(if (key-valid-p key)
(setq key (key-parse key))
(message "Invalid `consult-narrow-key' according to `key-valid-p': %S" key))
(dolist (pair consult--narrow-keys)
(define-key map (vconcat key (vector (car pair)))
(cons (cdr pair) #'consult-narrow))))
(when-let ((widen (consult--widen-key)))
(define-key map widen (cons "All" #'consult-narrow))))
(put #'consult-narrow-help 'completion-predicate #'ignore)
(put #'consult-narrow 'completion-predicate #'ignore)
(defun consult--split-perl (str &optional _plist)
"Split input STR in async input and filtering part.
The function returns a list with three elements: The async
string, the start position of the completion filter string and a
force flag. If the first character is a punctuation character it
determines the separator. Examples: \"/async/filter\",
\"#async#filter\"."
(if (string-match-p "^[[:punct:]]" str)
(save-match-data
(let ((q (regexp-quote (substring str 0 1))))
(string-match (concat "^" q "\\([^" q "]*\\)\\(" q "\\)?") str)
`(,(match-string 1 str)
,(match-end 0)
,(match-end 2)
(0 . ,(match-beginning 1))
,@(and (match-end 2) `((,(match-beginning 2) . ,(match-end 2)))))))
`(,str ,(length str))))
(defun consult--split-nil (str &optional _plist)
"Treat the complete input STR as async input."
`(,str ,(length str)))
(defun consult--split-separator (str plist)
"Split input STR in async input and filtering part at first separator.
PLIST is the splitter configuration, including the separator."
(let ((sep (regexp-quote (char-to-string (plist-get plist :separator)))))
(save-match-data
(if (string-match (format "^\\([^%s]+\\)\\(%s\\)?" sep sep) str)
`(,(match-string 1 str)
,(match-end 0)
,(match-end 2)
,@(and (match-end 2) `((,(match-beginning 2) . ,(match-end 2)))))
`(,str ,(length str))))))
(defun consult--split-setup (split)
"Setup splitting completion style with splitter function SPLIT."
(let* ((styles completion-styles)
(catdef completion-category-defaults)
(catovr completion-category-overrides)
(try (lambda (str table pred point)
(let ((completion-styles styles)
(completion-category-defaults catdef)
(completion-category-overrides catovr)
(pos (cadr (funcall split str))))
(pcase (completion-try-completion (substring str pos) table pred
(max 0 (- point pos)))
('t t)
(`(,newstr . ,newpt)
(cons (concat (substring str 0 pos) newstr)
(+ pos newpt)))))))
(all (lambda (str table pred point)
(let ((completion-styles styles)
(completion-category-defaults catdef)
(completion-category-overrides catovr)
(pos (cadr (funcall split str))))
(completion-all-completions (substring str pos) table pred
(max 0 (- point pos)))))))
(setq-local completion-styles-alist (cons `(consult--split ,try ,all "")
completion-styles-alist)
completion-styles '(consult--split)
completion-category-defaults nil
completion-category-overrides nil)))
(defmacro consult--with-async (bind &rest body)
"Setup asynchronous completion in BODY.
BIND is the asynchronous function binding."
(declare (indent 1))
(let ((async (car bind)))
`(let ((,async ,@(cdr bind))
(new-chunk (max read-process-output-max consult--process-chunk))
orig-chunk)
(consult--minibuffer-with-setup-hook
(:append
(lambda ()
(when (functionp ,async)
(setq orig-chunk read-process-output-max
read-process-output-max new-chunk)
(funcall ,async 'setup)
(let* ((mb (current-buffer))
(fun (lambda ()
(when-let (win (active-minibuffer-window))
(when (eq (window-buffer win) mb)
(with-current-buffer mb
(let ((inhibit-modification-hooks t))
(funcall ,async (minibuffer-contents-no-properties))))))))
(hook (make-symbol "consult--async-after-change-hook")))
(fset hook (lambda (&rest _) (run-at-time 0 nil fun)))
(add-hook 'after-change-functions hook nil 'local)
(funcall hook)))))
(let ((,async (if (functionp ,async) ,async (lambda (_) ,async))))
(unwind-protect
,(macroexp-progn body)
(funcall ,async 'destroy)
(when (and orig-chunk (eq read-process-output-max new-chunk))
(setq read-process-output-max orig-chunk))))))))
(defun consult--async-sink ()
"Create ASYNC sink function.
An async function must accept a single action argument. For the
\\='setup action it is guaranteed that the call originates from
the minibuffer. For the other actions no assumption about the
context can be made.
\\='setup Setup the internal closure state. Return nil.
\\='destroy Destroy the internal closure state. Return nil.
\\='flush Flush the list of candidates. Return nil.
\\='refresh Request UI refresh. Return nil.
nil Return the list of candidates.
list Append the list to the already existing candidates list and return it.
string Update with the current user input string. Return nil."
(let (candidates last buffer)
(lambda (action)
(pcase-exhaustive action
('setup
(setq buffer (current-buffer))
nil)
((or (pred stringp) 'destroy) nil)
('flush (setq candidates nil last nil))
('refresh
(when-let (win (active-minibuffer-window))
(when (eq (window-buffer win) buffer)
(with-selected-window win
(run-hooks 'consult--completion-refresh-hook)
(when (and consult--preview-function candidates)
(funcall consult--preview-function)))))
nil)
('nil candidates)
((pred consp)
(setq last (last (if last (setcdr last action) (setq candidates action))))
candidates)))))
(defun consult--async-split-style ()
"Return the async splitting style function and initial string."
(or (alist-get consult-async-split-style consult-async-split-styles-alist)
(user-error "Splitting style `%s' not found" consult-async-split-style)))
(defun consult--async-split-initial (initial)
"Return initial string for async command.
INITIAL is the additional initial string."
(concat (plist-get (consult--async-split-style) :initial) initial))
(defun consult--async-split-thingatpt (thing)
"Return THING at point with async initial prefix."
(when-let (str (thing-at-point thing))
(consult--async-split-initial str)))
(defun consult--async-split (async &optional split)
"Create async function, which splits the input string.
ASYNC is the async sink.
SPLIT is the splitting function."
(unless split
(let* ((style (consult--async-split-style))
(fn (plist-get style :function)))
(setq split (lambda (str) (funcall fn str style)))))
(lambda (action)
(pcase action
('setup
(consult--split-setup split)
(funcall async 'setup))
((pred stringp)
(pcase-let* ((`(,async-str ,_ ,force . ,highlights)
(funcall split action))
(async-len (length async-str))
(input-len (length action))
(prompt (minibuffer-prompt-end))
(field-beg prompt)
(field-idx 0))
(remove-list-of-text-properties prompt (+ prompt input-len) '(face field))
(dolist (hl highlights)
(put-text-property field-beg (+ prompt (cdr hl))
'field field-idx)
(put-text-property (+ prompt (car hl)) (+ prompt (cdr hl))
'face 'consult-async-split)
(setq field-beg (+ prompt (cdr hl))
field-idx (1+ field-idx)))
(funcall async
(if (or force (>= async-len consult-async-min-input))
async-str
""))))
(_ (funcall async action)))))
(defun consult--async-indicator (async)
"Create async function with a state indicator overlay.
ASYNC is the async sink."
(let (ov)
(lambda (action &optional state)
(pcase action
('indicator
(overlay-put ov 'display
(pcase-exhaustive state
('running #("*" 0 1 (face consult-async-running)))
('finished #(":" 0 1 (face consult-async-finished)))
('killed #(";" 0 1 (face consult-async-failed)))
('failed #("!" 0 1 (face consult-async-failed))))))
('setup
(setq ov (make-overlay (- (minibuffer-prompt-end) 2)
(- (minibuffer-prompt-end) 1)))
(funcall async 'setup))
('destroy
(delete-overlay ov)
(funcall async 'destroy))
(_ (funcall async action))))))
(defun consult--async-log (formatted &rest args)
"Log FORMATTED ARGS to variable `consult--async-log'."
(with-current-buffer (get-buffer-create consult--async-log)
(goto-char (point-max))
(insert (apply #'format formatted args))))
(defun consult--async-process (async builder &rest props)
"Create process source async function.
ASYNC is the async function which receives the candidates.
BUILDER is the command line builder function.
PROPS are optional properties passed to `make-process'."
(setq async (consult--async-indicator async))
(let (proc proc-buf last-args count)
(lambda (action)
(pcase action
("" (when proc
(delete-process proc)
(kill-buffer proc-buf)
(setq proc nil proc-buf nil))
(setq last-args nil))
((pred stringp)
(funcall async action)
(let* ((flush t)
(rest "")
(proc-filter
(lambda (_ out)
(when flush
(setq flush nil)
(funcall async 'flush))
(let ((lines (split-string out "[\r\n]+")))
(if (not (cdr lines))
(setq rest (concat rest (car lines)))
(setcar lines (concat rest (car lines)))
(let* ((len (length lines))
(last (nthcdr (- len 2) lines)))
(setq rest (cadr last)
count (+ count len -1))
(setcdr last nil)
(funcall async lines))))))
(proc-sentinel
(lambda (_ event)
(when flush
(setq flush nil)
(funcall async 'flush))
(funcall async 'indicator
(cond
((string-prefix-p "killed" event) 'killed)
((string-prefix-p "finished" event) 'finished)
(t 'failed)))
(when (and (string-prefix-p "finished" event) (not (equal rest "")))
(cl-incf count)
(funcall async (list rest)))
(consult--async-log
"consult--async-process sentinel: event=%s lines=%d\n"
(string-trim event) count)
(when (> (buffer-size proc-buf) 0)
(with-current-buffer (get-buffer-create consult--async-log)
(goto-char (point-max))
(insert ">>>>> stderr >>>>>\n")
(let ((beg (point)))
(insert-buffer-substring proc-buf)
(save-excursion
(goto-char beg)
(message #("%s" 0 2 (face error))
(buffer-substring-no-properties (pos-bol) (pos-eol)))))
(insert "<<<<< stderr <<<<<\n")))))
(args (funcall builder action)))
(unless (stringp (car args))
(setq args (car args)))
(unless (equal args last-args)
(setq last-args args)
(when proc
(delete-process proc)
(kill-buffer proc-buf)
(setq proc nil proc-buf nil))
(when args
(funcall async 'indicator 'running)
(consult--async-log "consult--async-process started %S\n" args)
(setq count 0
proc-buf (generate-new-buffer " *consult-async-stderr*")
proc (apply #'make-process
`(,@props
:connection-type pipe
:name ,(car args)
:stderr ,proc-buf
:noquery t
:command ,args
:filter ,proc-filter
:sentinel ,proc-sentinel))))))
nil)
('destroy
(when proc
(delete-process proc)
(kill-buffer proc-buf)
(setq proc nil proc-buf nil))
(funcall async 'destroy))
(_ (funcall async action))))))
(defun consult--async-highlight (async builder)
"Return ASYNC function which highlightes the candidates.
BUILDER is the command line builder function."
(let (highlight)
(lambda (action)
(cond
((stringp action)
(setq highlight (cdr (funcall builder action)))
(funcall async action))
((and (consp action) highlight)
(dolist (str action)
(funcall highlight str))
(funcall async action))
(t (funcall async action))))))
(defun consult--async-throttle (async &optional throttle debounce)
"Create async function from ASYNC which throttles input.
The THROTTLE delay defaults to `consult-async-input-throttle'.
The DEBOUNCE delay defaults to `consult-async-input-debounce'."
(setq throttle (or throttle consult-async-input-throttle)
debounce (or debounce consult-async-input-debounce))
(let ((input "") last timer)
(lambda (action)
(pcase action
((pred stringp)
(unless (equal action input)
(when timer
(cancel-timer timer)
(setq timer nil))
(funcall async "") (setq input action)
(unless (equal action "")
(setq timer
(run-at-time
(+ debounce
(if last
(min (- (float-time) last) throttle)
0))
nil
(lambda ()
(setq last (float-time))
(funcall async action))))))
nil)
('destroy
(when timer (cancel-timer timer))
(funcall async 'destroy))
(_ (funcall async action))))))
(defun consult--async-refresh-immediate (async)
"Create async function from ASYNC, which refreshes the display.
The refresh happens immediately when candidates are pushed."
(lambda (action)
(pcase action
((or (pred consp) 'flush)
(prog1 (funcall async action)
(funcall async 'refresh)))
(_ (funcall async action)))))
(defun consult--async-refresh-timer (async &optional delay)
"Create async function from ASYNC, which refreshes the display.
The refresh happens after a DELAY, defaulting to `consult-async-refresh-delay'."
(let ((timer) (refresh) (delay (or delay consult-async-refresh-delay)))
(lambda (action)
(prog1 (funcall async action)
(pcase action
((or (pred consp) 'flush)
(setq refresh t)
(unless timer
(setq timer (run-at-time
nil delay
(lambda ()
(when refresh
(setq refresh nil)
(funcall async 'refresh)))))))
('destroy (when timer (cancel-timer timer))))))))
(defmacro consult--async-command (builder &rest args)
"Asynchronous command pipeline.
ARGS is a list of `make-process' properties and transforms.
BUILDER is the command line builder function, which takes the
input string and must either return a list of command line
arguments or a pair of the command line argument list and a
highlighting function."
(declare (indent 1))
`(thread-first
(consult--async-sink)
(consult--async-refresh-timer)
,@(seq-take-while (lambda (x) (not (keywordp x))) args)
(consult--async-process
,builder
,@(seq-drop-while (lambda (x) (not (keywordp x))) args))
(consult--async-throttle)
(consult--async-split)))
(defmacro consult--async-transform (async &rest transform)
"Use FUN to TRANSFORM candidates of ASYNC."
(cl-with-gensyms (async-var action-var)
`(let ((,async-var ,async))
(lambda (,action-var)
(funcall ,async-var (if (consp ,action-var) (,@transform ,action-var) ,action-var))))))
(defun consult--async-map (async fun)
"Map candidates of ASYNC by FUN."
(consult--async-transform async mapcar fun))
(defun consult--async-filter (async fun)
"Filter candidates of ASYNC by FUN."
(consult--async-transform async seq-filter fun))
(defun consult--dynamic-compute (async fun &optional debounce)
"Dynamic computation of candidates.
ASYNC is the sink.
FUN computes the candidates given the input.
DEBOUNCE is the time after which an interrupted computation
should be restarted."
(setq debounce (or debounce consult-async-input-debounce))
(setq async (consult--async-indicator async))
(let* ((request) (current) (timer)
(cancel (lambda () (when timer (cancel-timer timer) (setq timer nil))))
(start (lambda (req) (setq request req) (funcall async 'refresh))))
(lambda (action)
(pcase action
((and 'nil (guard (not request)))
(funcall async nil))
('nil
(funcall cancel)
(let ((state 'killed))
(unwind-protect
(progn
(funcall async 'indicator 'running)
(redisplay)
(let ((response (funcall fun request)))
(funcall async 'flush)
(setq state 'finished current request)
(funcall async response)))
(funcall async 'indicator state)
(when (eq state 'killed)
(setq timer (run-at-time debounce nil start request)))
(setq request nil))))
((pred stringp)
(funcall cancel)
(if (or (equal action "") (equal action current))
(funcall async 'indicator 'finished)
(funcall start action)))
('destroy
(funcall cancel)
(funcall async 'destroy))
(_ (funcall async action))))))
(defun consult--dynamic-collection (fun)
"Dynamic collection with input splitting.
FUN computes the candidates given the input."
(thread-first
(consult--async-sink)
(consult--dynamic-compute fun)
(consult--async-throttle)
(consult--async-split)))
(defvar-keymap consult-async-map
:doc "Keymap added for commands with asynchronous candidates."
"<remap> <minibuffer-complete-word>" #'self-insert-command
"<remap> <minibuffer-complete-defaults>" #'ignore
"<remap> <minibuffer-complete-history>" #'consult-history)
(defvar-keymap consult-narrow-map
:doc "Narrowing keymap which is added to the local minibuffer map.
Note that `consult-narrow-key' and `consult-widen-key' are bound dynamically."
"SPC" consult--narrow-space
"DEL" consult--narrow-delete)
(defun consult--annotate-align (cand ann)
"Align annotation ANN by computing the maximum CAND width."
(setq consult--annotate-align-width
(max consult--annotate-align-width
(* (ceiling (consult--display-width cand)
consult--annotate-align-step)
consult--annotate-align-step)))
(when ann
(concat
#(" " 0 1 (display (space :align-to (+ left consult--annotate-align-width))))
ann)))
(defun consult--add-history (async items)
"Add ITEMS to the minibuffer future history.
ASYNC must be non-nil for async completion functions."
(delete-dups
(append
(ensure-list minibuffer-default)
(remove "" (remq nil (ensure-list items)))
(unless async
(all-completions ""
minibuffer-completion-table
minibuffer-completion-predicate)))))
(defun consult--setup-keymap (keymap async narrow preview-key)
"Setup minibuffer keymap.
KEYMAP is a command-specific keymap.
ASYNC must be non-nil for async completion functions.
NARROW are the narrow settings.
PREVIEW-KEY are the preview keys."
(let ((old-map (current-local-map))
(map (make-sparse-keymap)))
(when narrow
(consult--narrow-setup narrow map))
(when (and (consp preview-key) (memq :keys preview-key))
(setq preview-key (plist-get preview-key :keys)))
(setq preview-key (mapcar #'car (consult--preview-key-normalize preview-key)))
(when preview-key
(dolist (key preview-key)
(unless (or (eq key 'any) (lookup-key old-map key))
(define-key map key #'ignore))))
(use-local-map
(make-composed-keymap
(delq nil (list keymap
(and async consult-async-map)
(and narrow consult-narrow-map)
map))
old-map))))
(defun consult--tofu-hide-in-minibuffer (&rest _)
"Hide the tofus in the minibuffer."
(let* ((min (minibuffer-prompt-end))
(max (point-max))
(pos max))
(while (and (> pos min) (consult--tofu-p (char-before pos)))
(cl-decf pos))
(when (< pos max)
(add-text-properties pos max '(invisible t rear-nonsticky t cursor-intangible t)))))
(defun consult--read-annotate (fun cand)
"Annotate CAND with annotation function FUN."
(pcase (funcall fun cand)
(`(,_ ,_ ,suffix) suffix)
(ann ann)))
(defun consult--read-affixate (fun cands)
"Affixate CANDS with annotation function FUN."
(mapcar (lambda (cand)
(let ((ann (funcall fun cand)))
(if (consp ann)
ann
(setq ann (or ann ""))
(list cand ""
(if (text-property-not-all 0 (length ann) 'face nil ann)
ann
(propertize ann 'face 'completions-annotations))))))
cands))
(cl-defun consult--read-1 (candidates &key
prompt predicate require-match history default
keymap category initial narrow add-history annotate
state preview-key sort lookup group inherit-input-method)
"See `consult--read' for the documentation of the arguments."
(consult--minibuffer-with-setup-hook
(:append (lambda ()
(add-hook 'after-change-functions #'consult--tofu-hide-in-minibuffer nil 'local)
(consult--setup-keymap keymap (functionp candidates) narrow preview-key)
(setq-local minibuffer-default-add-function
(apply-partially #'consult--add-history (functionp candidates) add-history))))
(consult--with-async (async candidates)
(let* ((metadata `(metadata
,@(when category `((category . ,category)))
,@(when group `((group-function . ,group)))
,@(when annotate
`((affixation-function
. ,(apply-partially #'consult--read-affixate annotate))
(annotation-function
. ,(apply-partially #'consult--read-annotate annotate))))
,@(unless sort '((cycle-sort-function . identity)
(display-sort-function . identity)))))
(consult--annotate-align-width 0)
(result
(consult--with-preview
preview-key state
(lambda (narrow input cand)
(funcall lookup cand (funcall async nil) input narrow))
(apply-partially #'run-hook-with-args-until-success
'consult--completion-candidate-hook)
(completing-read prompt
(lambda (str pred action)
(if (eq action 'metadata)
metadata
(complete-with-action action (funcall async nil) str pred)))
predicate require-match initial
(if (symbolp history) history (cadr history))
default
inherit-input-method))))
(pcase-exhaustive history
(`(:input ,var)
(set var (cdr (symbol-value var)))
(add-to-history var (cdr result)))
((pred symbolp)))
(car result)))))
(cl-defun consult--read (candidates &rest options &key
prompt predicate require-match history default
keymap category initial narrow add-history annotate
state preview-key sort lookup group inherit-input-method)
"Enhanced completing read function to select from CANDIDATES.
The function is a thin wrapper around `completing-read'. Keyword
arguments are used instead of positional arguments for code
clarity. On top of `completing-read' it additionally supports
computing the candidate list asynchronously, candidate preview
and narrowing.
Keyword OPTIONS:
PROMPT is the string which is shown as prompt in the minibuffer.
PREDICATE is a filter function called for each candidate, returns
nil or t.
REQUIRE-MATCH equals t means that an exact match is required.
HISTORY is the symbol of the history variable.
DEFAULT is the default selected value.
ADD-HISTORY is a list of items to add to the history.
CATEGORY is the completion category symbol.
SORT should be set to nil if the candidates are already sorted.
This will disable sorting in the completion UI.
LOOKUP is a lookup function passed the selected candidate string,
the list of candidates, the current input string and the current
narrowing value.
ANNOTATE is a function passed a candidate string. The function
should either return an annotation string or a list of three
strings (candidate prefix postfix).
INITIAL is the initial input string.
STATE is the state function, see `consult--with-preview'.
GROUP is a completion metadata `group-function' as documented in
the Elisp manual.
PREVIEW-KEY are the preview keys. Can be nil, `any', a single
key or a list of keys.
NARROW is an alist of narrowing prefix strings and description.
KEYMAP is a command-specific keymap.
INHERIT-INPUT-METHOD, if non-nil the minibuffer inherits the
input method."
(cl-assert (or (functionp candidates) (obarrayp candidates) (hash-table-p candidates) (not candidates) (stringp (car candidates)) (and (consp (car candidates)) (stringp (caar candidates))) (and (consp (car candidates)) (symbolp (caar candidates))))) (ignore prompt predicate require-match history default
keymap category initial narrow add-history annotate
state preview-key sort lookup group inherit-input-method)
(apply #'consult--read-1 candidates
(append
(consult--customize-get)
options
(list :prompt "Select: "
:preview-key consult-preview-key
:sort t
:lookup (lambda (selected &rest _) selected)))))
(cl-defun consult--prompt-1 (&key prompt history add-history initial default
keymap state preview-key transform inherit-input-method)
"See `consult--prompt' for documentation."
(consult--minibuffer-with-setup-hook
(:append (lambda ()
(consult--setup-keymap keymap nil nil preview-key)
(setq-local minibuffer-default-add-function
(apply-partially #'consult--add-history nil add-history))))
(car (consult--with-preview
preview-key state
(lambda (_narrow inp _cand) (funcall transform inp))
(lambda () "")
(read-from-minibuffer prompt initial nil nil history default inherit-input-method)))))
(cl-defun consult--prompt (&rest options &key prompt history add-history initial default
keymap state preview-key transform inherit-input-method)
"Read from minibuffer.
Keyword OPTIONS:
PROMPT is the string to prompt with.
TRANSFORM is a function which is applied to the current input string.
HISTORY is the symbol of the history variable.
INITIAL is initial input.
DEFAULT is the default selected value.
ADD-HISTORY is a list of items to add to the history.
STATE is the state function, see `consult--with-preview'.
PREVIEW-KEY are the preview keys (nil, `any', a single key or a list of keys).
KEYMAP is a command-specific keymap."
(ignore prompt history add-history initial default
keymap state preview-key transform inherit-input-method)
(apply #'consult--prompt-1
(append
(consult--customize-get)
options
(list :prompt "Input: "
:preview-key consult-preview-key
:transform #'identity))))
(defsubst consult--multi-source (sources cand)
"Lookup source for CAND in SOURCES list."
(aref sources (consult--tofu-get cand)))
(defun consult--multi-predicate (sources cand)
"Predicate function called for each candidate CAND given SOURCES."
(let* ((src (consult--multi-source sources cand))
(narrow (plist-get src :narrow))
(type (or (car-safe narrow) narrow -1)))
(or (eq consult--narrow type)
(not (or consult--narrow (plist-get src :hidden))))))
(defun consult--multi-narrow (sources)
"Return narrow list from SOURCES."
(thread-last sources
(mapcar (lambda (src)
(when-let (narrow (plist-get src :narrow))
(if (consp narrow)
narrow
(when-let (name (plist-get src :name))
(cons narrow name))))))
(delq nil)
(delete-dups)))
(defun consult--multi-annotate (sources cand)
"Annotate candidate CAND from multi SOURCES."
(consult--annotate-align
cand
(let ((src (consult--multi-source sources cand)))
(if-let ((fun (plist-get src :annotate)))
(funcall fun (cdr (get-text-property 0 'multi-category cand)))
(plist-get src :name)))))
(defun consult--multi-group (sources cand transform)
"Return title of candidate CAND or TRANSFORM the candidate given SOURCES."
(if transform cand
(plist-get (consult--multi-source sources cand) :name)))
(defun consult--multi-preview-key (sources)
"Return preview keys from SOURCES."
(list :predicate
(lambda (cand)
(if (plist-member (cdr cand) :preview-key)
(plist-get (cdr cand) :preview-key)
consult-preview-key))
:keys
(delete-dups
(seq-mapcat (lambda (src)
(let ((key (if (plist-member src :preview-key)
(plist-get src :preview-key)
consult-preview-key)))
(ensure-list key)))
sources))))
(defun consult--multi-lookup (sources selected candidates _input narrow &rest _)
"Lookup SELECTED in CANDIDATES given SOURCES, with potential NARROW."
(if (or (string-blank-p selected)
(not (consult--tofu-p (aref selected (1- (length selected))))))
(let* ((src (cond
(narrow (seq-find (lambda (src)
(let ((n (plist-get src :narrow)))
(eq (or (car-safe n) n -1) narrow)))
sources))
((seq-find (lambda (src) (plist-get src :default)) sources))
((aref sources 0))))
(idx (seq-position sources src))
(def (and (string-blank-p selected) (seq-find (lambda (cand) (eq idx (consult--tofu-get cand))) candidates))))
(if def
(cons (cdr (get-text-property 0 'multi-category def)) src)
`(,selected :match nil ,@src)))
(if-let (found (member selected candidates))
(cons (cdr (get-text-property 0 'multi-category (car found)))
(consult--multi-source sources selected))
`(,(substring selected 0 -1) :match nil ,@(consult--multi-source sources selected)))))
(defun consult--multi-candidates (sources)
"Return `consult--multi' candidates from SOURCES."
(let ((idx 0) candidates)
(seq-doseq (src sources)
(let* ((face (and (plist-member src :face) `(face ,(plist-get src :face))))
(cat (plist-get src :category))
(items (plist-get src :items))
(items (if (functionp items) (funcall items) items)))
(dolist (item items)
(let ((cand (consult--tofu-append item idx)))
(if (get-text-property 0 'multi-category cand)
(when face (add-text-properties 0 (length item) face cand))
(add-text-properties 0 (length item)
`(multi-category (,cat . ,item) ,@face) cand))
(push cand candidates))))
(cl-incf idx))
(nreverse candidates)))
(defun consult--multi-enabled-sources (sources)
"Return vector of enabled SOURCES."
(vconcat
(seq-filter (lambda (src)
(if-let (pred (plist-get src :enabled))
(funcall pred)
t))
(mapcar (lambda (src)
(if (symbolp src) (symbol-value src) src))
sources))))
(defun consult--multi-state (sources)
"State function given SOURCES."
(when-let (states (delq nil (mapcar (lambda (src)
(when-let (fun (plist-get src :state))
(cons src (funcall fun))))
sources)))
(let (last-fun)
(pcase-lambda (action `(,cand . ,src))
(pcase action
('setup
(pcase-dolist (`(,_ . ,fun) states)
(funcall fun 'setup nil)))
('exit
(pcase-dolist (`(,_ . ,fun) states)
(funcall fun 'exit nil)))
('preview
(let ((selected-fun (cdr (assq src states))))
(when (and last-fun (not (eq last-fun selected-fun)))
(funcall last-fun 'preview nil))
(setq last-fun selected-fun)
(when selected-fun
(funcall selected-fun 'preview cand))))
('return
(let ((selected-fun (cdr (assq src states))))
(pcase-dolist (`(,_ . ,fun) states)
(unless (eq fun selected-fun)
(funcall fun 'return nil)))
(when selected-fun
(funcall selected-fun 'return cand)))))))))
(defun consult--multi (sources &rest options)
"Select from candidates taken from a list of SOURCES.
OPTIONS is the plist of options passed to `consult--read'. The following
options are supported: :require-match, :history, :keymap, :initial,
:add-history, :sort and :inherit-input-method. The other options of
`consult--read' are used by the implementation of `consult--multi' and
should not be overwritten, except in in special scenarios.
The function returns the selected candidate in the form (cons candidate
source-plist). The plist has the key :match with a value nil if the
candidate does not exist, t if the candidate exists and `new' if the
candidate has been created. The sources of the source list can either be
symbols of source variables or source values. Source values must be
plists with fields from the following list.
Required source fields:
* :category - Completion category symbol.
* :items - List of strings to select from or function returning
list of strings. Note that the strings can use text properties
to carry metadata, which is then available to the :annotate,
:action and :state functions.
Optional source fields:
* :name - Name of the source as a string, used for narrowing,
group titles and annotations.
* :narrow - Narrowing character or (character . string) pair.
* :enabled - Function which must return t if the source is enabled.
* :hidden - When t candidates of this source are hidden by default.
* :face - Face used for highlighting the candidates.
* :annotate - Annotation function called for each candidate, returns string.
* :history - Name of history variable to add selected candidate.
* :default - Must be t if the first item of the source is the default value.
* :action - Function called with the selected candidate.
* :new - Function called with new candidate name, only if :require-match is nil.
* :state - State constructor for the source, must return the
state function. The state function is informed about state
changes of the UI and can be used to implement preview.
* Other custom source fields can be added depending on the use
case. Note that the source is returned by `consult--multi'
together with the selected candidate."
(let* ((sources (consult--multi-enabled-sources sources))
(candidates (consult--with-increased-gc
(consult--multi-candidates sources)))
(selected
(apply #'consult--read
candidates
(append
options
(list
:category 'multi-category
:predicate (apply-partially #'consult--multi-predicate sources)
:annotate (apply-partially #'consult--multi-annotate sources)
:group (apply-partially #'consult--multi-group sources)
:lookup (apply-partially #'consult--multi-lookup sources)
:preview-key (consult--multi-preview-key sources)
:narrow (consult--multi-narrow sources)
:state (consult--multi-state sources))))))
(when-let (history (plist-get (cdr selected) :history))
(add-to-history history (car selected)))
(if (plist-member (cdr selected) :match)
(when-let (fun (plist-get (cdr selected) :new))
(funcall fun (car selected))
(plist-put (cdr selected) :match 'new))
(when-let (fun (plist-get (cdr selected) :action))
(funcall fun (car selected)))
(setq selected `(,(car selected) :match t ,@(cdr selected))))
selected))
(defun consult--customize-put (cmds prop form)
"Set property PROP to FORM of commands CMDS."
(dolist (cmd cmds)
(cond
((and (boundp cmd) (consp (symbol-value cmd)))
(setf (plist-get (symbol-value cmd) prop) (eval form 'lexical)))
((functionp cmd)
(setf (plist-get (alist-get cmd consult--customize-alist) prop) form))
(t (user-error "%s is neither a Command command nor a source" cmd))))
nil)
(defmacro consult-customize (&rest args)
"Set properties of commands or sources.
ARGS is a list of commands or sources followed by the list of
keyword-value pairs. For `consult-customize' to succeed, the
customized sources and commands must exist. When a command is
invoked, the value of `this-command' is used to lookup the
corresponding customization options."
(let (setter)
(while args
(let ((cmds (seq-take-while (lambda (x) (not (keywordp x))) args)))
(setq args (seq-drop-while (lambda (x) (not (keywordp x))) args))
(while (keywordp (car args))
(push `(consult--customize-put ',cmds ,(car args) ',(cadr args)) setter)
(setq args (cddr args)))))
(macroexp-progn setter)))
(defun consult--customize-get (&optional cmd)
"Get configuration from `consult--customize-alist' for CMD."
(mapcar (lambda (x) (eval x 'lexical))
(alist-get (or cmd this-command) consult--customize-alist)))
(defun consult--insertion-preview (start end)
"State function for previewing a candidate in a specific region.
The candidates are previewed in the region from START to END. This function is
used as the `:state' argument for `consult--read' in the `consult-yank' family
of functions and in `consult-completion-in-region'."
(unless (or (minibufferp)
(not (eq (window-buffer) (current-buffer)))
(and (markerp start) (not (eq (marker-buffer start) (current-buffer))))
(and (markerp end) (not (eq (marker-buffer end) (current-buffer)))))
(let (ov)
(lambda (action cand)
(cond
((and (not cand) ov)
(delete-overlay ov)
(setq ov nil))
((and (eq action 'preview) cand)
(unless ov
(setq ov (consult--make-overlay start end
'invisible t
'window (selected-window))))
(setq cand (copy-sequence cand))
(add-face-text-property 0 (length cand) 'consult-preview-insertion t cand)
(overlay-put ov 'before-string cand)))))))
(defun consult-completion-in-region (start end collection &optional predicate)
"Use minibuffer completion as the UI for `completion-at-point'.
The function is called with 4 arguments: START END COLLECTION PREDICATE.
The arguments and expected return value are as specified for
`completion-in-region'. Use as a value for `completion-in-region-function'.
The function can be configured via `consult-customize'.
(consult-customize consult-completion-in-region
:completion-styles (basic)
:cycle-threshold 3)
These configuration options are supported:
* :cycle-threshold - Cycling threshold (def: `completion-cycle-threshold')
* :completion-styles - Use completion styles (def: `completion-styles')
* :require-match - Require matches when completing (def: nil)
* :prompt - The prompt string shown in the minibuffer"
(barf-if-buffer-read-only)
(cl-letf* ((config (consult--customize-get #'consult-completion-in-region))
(cs (or (plist-get config :completion-styles) completion-styles))
(completion-styles cs)
((default-value 'completion-styles) cs)
(prompt (or (plist-get config :prompt) "Completion: "))
(require-match (plist-get config :require-match))
(preview-key (if (plist-member config :preview-key)
(plist-get config :preview-key)
consult-preview-key))
(initial (buffer-substring-no-properties start end))
(metadata (completion-metadata initial collection predicate))
(threshold (or (plist-get config :cycle-threshold) (completion--cycle-threshold metadata)))
(all (completion-all-completions initial collection predicate (length initial)))
(exit-fun (plist-get completion-extra-properties :exit-function))
(ann-fun (plist-get completion-extra-properties :annotation-function))
(aff-fun (plist-get completion-extra-properties :affixation-function))
(docsig-fun (plist-get completion-extra-properties :company-docsig))
(completion-extra-properties
`(,@(and ann-fun (list :annotation-function (consult--in-buffer ann-fun)))
,@(and aff-fun (list :affixation-function (consult--in-buffer aff-fun)))
,@(and docsig-fun (not ann-fun) (not aff-fun)
(list :annotation-function
(consult--in-buffer
(lambda (cand)
(concat (propertize " " 'display '(space :align-to center))
(funcall docsig-fun cand)))))))))
(if (and threshold
(or (not (consp (ignore-errors (nthcdr threshold all))))
(and completion-cycling completion-all-sorted-completions)))
(completion--in-region start end collection predicate)
(let* ((limit (car (completion-boundaries initial collection predicate "")))
(category (completion-metadata-get metadata 'category))
(completion
(cond
((atom all) nil)
((and (consp all) (atom (cdr all)))
(concat (substring initial 0 limit) (car all)))
(t (car
(consult--with-preview
preview-key
(consult--insertion-preview start end)
(if (eq category 'file)
(cond
((file-name-absolute-p initial)
(lambda (_narrow _inp cand)
(substitute-in-file-name cand)))
((string-match-p "\\`\\.\\.?/" initial)
(lambda (_narrow _inp cand)
(setq cand (file-relative-name (substitute-in-file-name cand)))
(if (string-match-p "\\`\\.\\.?/" cand) cand (concat "./" cand))))
(t
(lambda (_narrow _inp cand)
(file-relative-name (substitute-in-file-name cand)))))
(lambda (_narrow _inp cand) cand))
(apply-partially #'run-hook-with-args-until-success
'consult--completion-candidate-hook)
(consult--local-let ((enable-recursive-minibuffers t))
(if (eq category 'file)
(consult--minibuffer-with-setup-hook
(lambda () (insert initial))
(read-file-name prompt nil initial require-match nil predicate))
(completing-read prompt
(consult--completion-table-in-buffer collection)
predicate require-match initial)))))))))
(if completion
(progn
(completion--replace start end (setq completion (concat completion)))
(when exit-fun
(funcall exit-fun completion
(if (eq (try-completion completion collection predicate) t)
'finished 'exact)))
t)
(message "No completion")
nil)))))
(defun consult--outline-candidates ()
"Return alist of outline headings and positions."
(consult--forbid-minibuffer)
(let* ((line (line-number-at-pos (point-min) consult-line-numbers-widen))
(heading-regexp (concat "^\\(?:"
(or (bound-and-true-p outline-regexp) "[*\^L]+")
"\\)"))
(heading-alist (bound-and-true-p outline-heading-alist))
(level-fun (or (bound-and-true-p outline-level)
(lambda () (or (cdr (assoc (match-string 0) heading-alist))
(- (match-end 0) (match-beginning 0))))))
(buffer (current-buffer))
candidates)
(save-excursion
(goto-char (point-min))
(while (save-excursion
(if-let (fun (bound-and-true-p outline-search-function))
(funcall fun)
(re-search-forward heading-regexp nil t)))
(cl-incf line (consult--count-lines (match-beginning 0)))
(push (consult--location-candidate
(consult--buffer-substring (pos-bol) (pos-eol) 'fontify)
(cons buffer (point)) (1- line) (1- line)
'consult--outline-level (funcall level-fun))
candidates)
(goto-char (1+ (pos-eol)))))
(unless candidates
(user-error "No headings"))
(nreverse candidates)))
(defun consult-outline ()
"Jump to an outline heading, obtained by matching against `outline-regexp'.
This command supports narrowing to a heading level and candidate preview.
The symbol at point is added to the future history."
(interactive)
(let* ((candidates (consult--slow-operation
"Collecting headings..."
(consult--outline-candidates)))
(min-level (- (apply #'min (mapcar
(lambda (cand)
(get-text-property 0 'consult--outline-level cand))
candidates))
?1))
(narrow-pred (lambda (cand)
(<= (get-text-property 0 'consult--outline-level cand)
(+ consult--narrow min-level))))
(narrow-keys (mapcar (lambda (c) (cons c (format "Level %c" c)))
(number-sequence ?1 ?9))))
(consult--read
candidates
:prompt "Go to heading: "
:annotate (consult--line-prefix)
:category 'consult-location
:sort nil
:require-match t
:lookup #'consult--line-match
:narrow `(:predicate ,narrow-pred :keys ,narrow-keys)
:history '(:input consult--line-history)
:add-history (thing-at-point 'symbol)
:state (consult--location-state candidates))))
(defun consult--mark-candidates (markers)
"Return list of candidates strings for MARKERS."
(consult--forbid-minibuffer)
(let ((candidates)
(current-buf (current-buffer)))
(save-excursion
(dolist (marker markers)
(when-let ((pos (marker-position marker))
(buf (marker-buffer marker)))
(when (and (eq buf current-buf)
(consult--in-range-p pos))
(goto-char pos)
(push (consult--location-candidate
(consult--line-with-cursor marker) marker
(line-number-at-pos pos consult-line-numbers-widen)
marker)
candidates)))))
(unless candidates
(user-error "No marks"))
(nreverse (delete-dups candidates))))
(defun consult-mark (&optional markers)
"Jump to a marker in MARKERS list (defaults to buffer-local `mark-ring').
The command supports preview of the currently selected marker position.
The symbol at point is added to the future history."
(interactive)
(consult--read
(consult--mark-candidates
(or markers (cons (mark-marker) mark-ring)))
:prompt "Go to mark: "
:annotate (consult--line-prefix)
:category 'consult-location
:sort nil
:require-match t
:lookup #'consult--lookup-location
:history '(:input consult--line-history)
:add-history (thing-at-point 'symbol)
:state (consult--jump-state)))
(defun consult--global-mark-candidates (markers)
"Return list of candidates strings for MARKERS."
(consult--forbid-minibuffer)
(let ((candidates))
(save-excursion
(dolist (marker markers)
(when-let ((pos (marker-position marker))
(buf (marker-buffer marker)))
(unless (minibufferp buf)
(with-current-buffer buf
(when (consult--in-range-p pos)
(goto-char pos)
(let ((line (line-number-at-pos pos consult-line-numbers-widen)))
(push (concat
(propertize
(consult--format-file-line-match (buffer-name buf) line "")
'consult-location (cons marker line)
'consult-strip t)
(consult--line-with-cursor marker)
(consult--tofu-encode marker))
candidates))))))))
(unless candidates
(user-error "No global marks"))
(nreverse (delete-dups candidates))))
(defun consult-global-mark (&optional markers)
"Jump to a marker in MARKERS list (defaults to `global-mark-ring').
The command supports preview of the currently selected marker position.
The symbol at point is added to the future history."
(interactive)
(consult--read
(consult--global-mark-candidates
(or markers global-mark-ring))
:prompt "Go to global mark: "
:category 'consult-location
:sort nil
:require-match t
:lookup #'consult--lookup-location
:history '(:input consult--line-history)
:add-history (thing-at-point 'symbol)
:state (consult--jump-state)))
(defun consult--line-candidates (top curr-line)
"Return list of line candidates.
Start from top if TOP non-nil.
CURR-LINE is the current line number."
(consult--forbid-minibuffer)
(consult--fontify-all)
(let* ((buffer (current-buffer))
(line (line-number-at-pos (point-min) consult-line-numbers-widen))
default-cand candidates)
(consult--each-line beg end
(unless (looking-at-p "^\\s-*$")
(push (consult--location-candidate
(consult--buffer-substring beg end)
(cons buffer beg) line line)
candidates)
(when (and (not default-cand) (>= line curr-line))
(setq default-cand candidates)))
(cl-incf line))
(unless candidates
(user-error "No lines"))
(nreverse
(if (or top (not default-cand))
candidates
(let ((before (cdr default-cand)))
(setcdr default-cand nil)
(nconc before candidates))))))
(defun consult--line-point-placement (selected candidates highlighted &rest ignored-faces)
"Find point position on matching line.
SELECTED is the currently selected candidate.
CANDIDATES is the list of candidates.
HIGHLIGHTED is the highlighted string to determine the match position.
IGNORED-FACES are ignored when determining the match position."
(when-let (pos (consult--lookup-location selected candidates))
(if highlighted
(let* ((matches (apply #'consult--point-placement highlighted 0 ignored-faces))
(dest (+ pos (car matches))))
(when (and (markerp pos) (not (eq (marker-buffer pos) (current-buffer))))
(setq dest (move-marker (make-marker) dest (marker-buffer pos))))
(cons dest (cdr matches)))
pos)))
(defun consult--line-match (selected candidates input &rest _)
"Lookup position of match.
SELECTED is the currently selected candidate.
CANDIDATES is the list of candidates.
INPUT is the input string entered by the user."
(consult--line-point-placement selected candidates
(and (not (string-blank-p input))
(car (consult--completion-filter
input
(list (substring-no-properties selected))
'consult-location 'highlight)))
'completions-first-difference))
(defun consult-line (&optional initial start)
"Search for a matching line.
Depending on the setting `consult-point-placement' the command
jumps to the beginning or the end of the first match on the line
or the line beginning. The default candidate is the non-empty
line next to point. This command obeys narrowing. Optional
INITIAL input can be provided. The search starting point is
changed if the START prefix argument is set. The symbol at point
and the last `isearch-string' is added to the future history."
(interactive (list nil (not (not current-prefix-arg))))
(let* ((curr-line (line-number-at-pos (point) consult-line-numbers-widen))
(top (not (eq start consult-line-start-from-top)))
(candidates (consult--slow-operation "Collecting lines..."
(consult--line-candidates top curr-line))))
(consult--read
candidates
:prompt (if top "Go to line from top: " "Go to line: ")
:annotate (consult--line-prefix curr-line)
:category 'consult-location
:sort nil
:require-match t
:add-history (list (thing-at-point 'symbol) isearch-string)
:history '(:input consult--line-history)
:lookup #'consult--line-match
:default (car candidates)
:initial (or initial
(and isearch-mode
(prog1 isearch-string (isearch-done))))
:state (consult--location-state candidates))))
(defun consult--line-multi-match (selected candidates &rest _)
"Lookup position of match.
SELECTED is the currently selected candidate.
CANDIDATES is the list of candidates."
(consult--line-point-placement selected candidates
(car (member selected candidates))))
(defun consult--line-multi-group (cand transform)
"Group function used by `consult-line-multi'.
If TRANSFORM non-nil, return transformed CAND, otherwise return title."
(if transform cand
(let ((marker (car (get-text-property 0 'consult-location cand))))
(buffer-name
(if (consp marker)
(car marker)
(marker-buffer marker))))))
(defun consult--line-multi-candidates (buffers input)
"Collect matching candidates from multiple buffers.
INPUT is the user input which should be matched.
BUFFERS is the list of buffers."
(pcase-let ((`(,regexps . ,hl)
(funcall consult--regexp-compiler
input 'emacs completion-ignore-case))
(candidates nil)
(cand-idx 0))
(save-match-data
(dolist (buf buffers (nreverse candidates))
(with-current-buffer buf
(save-excursion
(let ((line (line-number-at-pos (point-min) consult-line-numbers-widen)))
(goto-char (point-min))
(while (and (not (eobp))
(save-excursion (re-search-forward (car regexps) nil t)))
(cl-incf line (consult--count-lines (match-beginning 0)))
(let ((bol (pos-bol))
(eol (pos-eol)))
(goto-char bol)
(when (and (not (looking-at-p "^\\s-*$"))
(seq-every-p (lambda (r)
(goto-char bol)
(re-search-forward r eol t))
(cdr regexps)))
(push (consult--location-candidate
(funcall hl (buffer-substring-no-properties bol eol))
(cons buf bol) (1- line) cand-idx)
candidates)
(cl-incf cand-idx))
(goto-char (1+ eol)))))))))))
(defun consult-line-multi (query &optional initial)
"Search for a matching line in multiple buffers.
By default search across all project buffers. If the prefix
argument QUERY is non-nil, all buffers are searched. Optional
INITIAL input can be provided. The symbol at point and the last
`isearch-string' is added to the future history.In order to
search a subset of buffers, QUERY can be set to a plist according
to `consult--buffer-query'."
(interactive "P")
(unless (keywordp (car-safe query))
(setq query (list :sort 'alpha-current :directory (and (not query) 'project))))
(pcase-let* ((`(,prompt . ,buffers) (consult--buffer-query-prompt "Go to line" query))
(collection (consult--dynamic-collection
(apply-partially #'consult--line-multi-candidates
buffers))))
(consult--read
collection
:prompt prompt
:annotate (consult--line-prefix)
:category 'consult-location
:sort nil
:require-match t
:add-history (mapcar #'consult--async-split-initial
(delq nil (list (thing-at-point 'symbol)
isearch-string)))
:history '(:input consult--line-multi-history)
:lookup #'consult--line-multi-match
:initial (consult--async-split-initial
(or initial
(and isearch-mode
(prog1 isearch-string (isearch-done)))))
:state (consult--location-state (lambda () (funcall collection nil)))
:group #'consult--line-multi-group)))
(defun consult--keep-lines-state (filter)
"State function for `consult-keep-lines' with FILTER function."
(let ((font-lock-orig font-lock-mode)
(hl-line-orig (bound-and-true-p hl-line-mode))
(point-orig (point))
lines content-orig replace last-input)
(if (use-region-p)
(save-restriction
(let ((rbeg (region-beginning))
(rend (save-excursion
(goto-char (region-end))
(unless (or (bolp) (eobp))
(forward-line 0))
(point))))
(consult--fontify-region rbeg rend)
(narrow-to-region rbeg rend)
(consult--each-line beg end
(push (consult--buffer-substring beg end) lines))
(setq content-orig (buffer-string)
replace (lambda (content &optional pos)
(delete-region rbeg rend)
(insert-before-markers content)
(goto-char (or pos rbeg))
(setq rend (+ rbeg (length content)))
(add-face-text-property rbeg rend 'region t)))))
(consult--fontify-all)
(setq content-orig (buffer-string)
replace (lambda (content &optional pos)
(delete-region (point-min) (point-max))
(insert content)
(goto-char (or pos (point-min)))))
(consult--each-line beg end
(push (consult--buffer-substring beg end) lines)))
(setq lines (nreverse lines))
(lambda (action input)
(when (and (eq action 'return) last-input)
(with-silent-modifications (funcall replace content-orig point-orig)))
(when (and input (or
(and (eq action 'return) (not (string-match-p "\\`!? ?\\'" input)))
(not (equal input last-input))))
(let ((filtered-content
(if (string-match-p "\\`!? ?\\'" input)
content-orig
(if (eq action 'return)
(apply #'concat (mapcan (lambda (x) (list x "\n"))
(funcall filter input lines)))
(while-no-input
(apply #'concat (mapcan (lambda (x) (list x "\n"))
(funcall filter input (mapcar #'copy-sequence lines)))))))))
(when (stringp filtered-content)
(when font-lock-mode (font-lock-mode -1))
(when (bound-and-true-p hl-line-mode) (hl-line-mode -1))
(if (eq action 'return)
(atomic-change-group
(let ((inhibit-modification-hooks t))
(funcall replace filtered-content)))
(with-silent-modifications
(funcall replace filtered-content)
(setq last-input input))))))
(when (eq action 'return)
(when hl-line-orig (hl-line-mode 1))
(when font-lock-orig (font-lock-mode 1))))))
(defun consult-keep-lines (&optional filter initial)
"Select a subset of the lines in the current buffer with live preview.
The selected lines are kept and the other lines are deleted. When called
interactively, the lines selected are those that match the minibuffer input. In
order to match the inverse of the input, prefix the input with `! '. When
called from elisp, the filtering is performed by a FILTER function. This
command obeys narrowing.
FILTER is the filter function.
INITIAL is the initial input."
(interactive
(list (lambda (pattern cands)
(consult--completion-filter-dispatch
pattern cands 'consult-location 'highlight))))
(consult--forbid-minibuffer)
(let ((ro buffer-read-only))
(unwind-protect
(consult--minibuffer-with-setup-hook
(lambda ()
(when ro
(minibuffer-message
(substitute-command-keys
" [Unlocked read-only buffer. \\[minibuffer-keyboard-quit] to quit.]"))))
(setq buffer-read-only nil)
(consult--with-increased-gc
(consult--prompt
:prompt "Keep lines: "
:initial initial
:history 'consult--keep-lines-history
:state (consult--keep-lines-state filter))))
(setq buffer-read-only ro))))
(defun consult--focus-lines-state (filter)
"State function for `consult-focus-lines' with FILTER function."
(let (lines overlays last-input pt-orig pt-min pt-max)
(save-excursion
(save-restriction
(if (not (use-region-p))
(consult--fontify-all)
(consult--fontify-region (region-beginning) (region-end))
(narrow-to-region
(region-beginning)
(save-excursion
(goto-char (region-end))
(unless (or (bolp) (eobp))
(forward-line 0))
(point))))
(setq pt-orig (point) pt-min (point-min) pt-max (point-max))
(let ((i 0))
(consult--each-line beg end
(let ((line (if (eq beg end) (char-to-string ?\n)
(buffer-substring-no-properties beg end))))
(put-text-property 0 1 'consult--focus-line (cons (cl-incf i) beg) line)
(push line lines)))
(setq lines (nreverse lines)))))
(lambda (action input)
(when (and input (not (equal input last-input)))
(let (new-overlays)
(pcase (while-no-input
(unless (string-match-p "\\`!? ?\\'" input) (let* ((inhibit-quit (eq action 'return)) (not (string-prefix-p "! " input))
(stripped (string-remove-prefix "! " input))
(matches (funcall filter stripped lines))
(old-ind 0)
(block-beg pt-min)
(block-end pt-min))
(while old-ind
(let ((match (pop matches)) (ind nil) (beg pt-max) (end pt-max) prop)
(when match
(setq prop (get-text-property 0 'consult--focus-line match)
ind (car prop)
beg (cdr prop)
end (+ 1 beg (if (equal match "\n") 0 (length match)))))
(unless (eq ind (1+ old-ind))
(let ((a (if not block-beg block-end))
(b (if not block-end beg)))
(when (/= a b)
(push (consult--make-overlay a b 'invisible t) new-overlays)))
(setq block-beg beg))
(setq block-end end old-ind ind)))))
'commit)
('commit
(mapc #'delete-overlay overlays)
(setq last-input input overlays new-overlays))
(_ (mapc #'delete-overlay new-overlays)))))
(when (eq action 'return)
(cond
((not input)
(mapc #'delete-overlay overlays)
(goto-char pt-orig))
((equal input "")
(consult-focus-lines 'show)
(goto-char pt-orig))
(t
(setq consult--focus-lines-overlays
(nconc consult--focus-lines-overlays overlays))
(goto-char (if-let (ov (and (invisible-p pt-orig)
(seq-find (lambda (ov) (overlay-get ov 'invisible))
(overlays-at pt-orig))))
(overlay-end ov)
pt-orig))))))))
(defun consult-focus-lines (&optional show filter initial)
"Hide or show lines using overlays.
The selected lines are shown and the other lines hidden. When called
interactively, the lines selected are those that match the minibuffer input. In
order to match the inverse of the input, prefix the input with `! '. With
optional prefix argument SHOW reveal the hidden lines. Alternatively the
command can be restarted to reveal the lines. When called from elisp, the
filtering is performed by a FILTER function. This command obeys narrowing.
FILTER is the filter function.
INITIAL is the initial input."
(interactive
(list current-prefix-arg
(lambda (pattern cands)
(consult--completion-filter-dispatch
pattern cands 'consult-location nil))))
(if show
(progn
(mapc #'delete-overlay consult--focus-lines-overlays)
(setq consult--focus-lines-overlays nil)
(message "All lines revealed"))
(consult--forbid-minibuffer)
(consult--with-increased-gc
(consult--prompt
:prompt
(if consult--focus-lines-overlays
"Focus on lines (RET to reveal): "
"Focus on lines: ")
:initial initial
:history 'consult--keep-lines-history
:state (consult--focus-lines-state filter)))))
(defun consult--goto-line-position (str msg)
"Transform input STR to line number.
Print an error message with MSG function."
(if-let (line (and str
(string-match-p "\\`[[:digit:]]+\\'" str)
(string-to-number str)))
(let ((pos (save-excursion
(save-restriction
(when consult-line-numbers-widen
(widen))
(goto-char (point-min))
(forward-line (1- line))
(point)))))
(if (consult--in-range-p pos)
pos
(funcall msg "Line number out of range.")
nil))
(when (and str (not (equal str "")))
(funcall msg "Please enter a number."))
nil))
(defun consult-goto-line (&optional arg)
"Read line number and jump to the line with preview.
Jump directly if a line number is given as prefix ARG. The command respects
narrowing and the settings `consult-goto-line-numbers' and
`consult-line-numbers-widen'."
(interactive "P")
(if arg
(call-interactively #'goto-line)
(consult--forbid-minibuffer)
(consult--local-let ((display-line-numbers consult-goto-line-numbers)
(display-line-numbers-widen consult-line-numbers-widen))
(while (if-let (pos (consult--goto-line-position
(consult--prompt
:prompt "Go to line: "
:history 'goto-line-history
:state
(let ((preview (consult--jump-preview)))
(lambda (action str)
(funcall preview action
(consult--goto-line-position str #'ignore)))))
#'minibuffer-message))
(consult--jump pos)
t)))))
(defun consult--file-preview ()
"Create preview function for files."
(let ((open (consult--temporary-files))
(preview (consult--buffer-preview)))
(lambda (action cand)
(unless cand
(funcall open))
(funcall preview action
(and cand
(eq action 'preview)
(funcall open cand))))))
(defun consult--file-action (file)
"Open FILE via `consult--buffer-action'."
(consult--buffer-action (find-file-noselect file)))
(consult--define-state file)
(defun consult-recent-file ()
"Find recent file using `completing-read'."
(interactive)
(find-file
(consult--read
(or
(let (file-name-handler-alist) (mapcar #'abbreviate-file-name (bound-and-true-p recentf-list)))
(user-error "No recent files, `recentf-mode' is %s"
(if recentf-mode "enabled" "disabled")))
:prompt "Find recent file: "
:sort nil
:require-match t
:category 'file
:state (consult--file-preview)
:history 'file-name-history)))
(defun consult--mode-name (mode)
"Return name part of MODE."
(replace-regexp-in-string
"global-\\(.*\\)-mode" "\\1"
(replace-regexp-in-string
"\\(-global\\)?-mode\\'" ""
(if (eq mode 'c-mode)
"cc"
(symbol-name mode))
'fixedcase)
'fixedcase))
(defun consult--mode-command-candidates (modes)
"Extract commands from MODES.
The list of features is searched for files belonging to the modes.
From these files, the commands are extracted."
(let* ((buffer (current-buffer))
(command-filter (consult--regexp-filter (seq-filter #'stringp consult-mode-command-filter)))
(feature-filter (seq-filter #'symbolp consult-mode-command-filter))
(minor-hash (consult--string-hash minor-mode-list))
(minor-local-modes (seq-filter (lambda (m)
(and (gethash m minor-hash)
(local-variable-if-set-p m)))
modes))
(minor-global-modes (seq-filter (lambda (m)
(and (gethash m minor-hash)
(not (local-variable-if-set-p m))))
modes))
(major-modes (seq-remove (lambda (m)
(gethash m minor-hash))
modes))
(major-paths-hash (consult--string-hash (mapcar #'symbol-file major-modes)))
(minor-local-paths-hash (consult--string-hash (mapcar #'symbol-file minor-local-modes)))
(minor-global-paths-hash (consult--string-hash (mapcar #'symbol-file minor-global-modes)))
(major-name-regexp (regexp-opt (mapcar #'consult--mode-name major-modes)))
(minor-local-name-regexp (regexp-opt (mapcar #'consult--mode-name minor-local-modes)))
(minor-global-name-regexp (regexp-opt (mapcar #'consult--mode-name minor-global-modes)))
(commands))
(dolist (feature load-history commands)
(when-let (name (alist-get 'provide feature))
(let* ((path (car feature))
(file (file-name-nondirectory path))
(key (cond
((memq name feature-filter) nil)
((or (gethash path major-paths-hash)
(string-match-p major-name-regexp file))
?m)
((or (gethash path minor-local-paths-hash)
(string-match-p minor-local-name-regexp file))
?l)
((or (gethash path minor-global-paths-hash)
(string-match-p minor-global-name-regexp file))
?g))))
(when key
(dolist (cmd (cdr feature))
(let ((sym (cdr-safe cmd)))
(when (and (consp cmd)
(eq (car cmd) 'defun)
(commandp sym)
(not (get sym 'byte-obsolete-info))
(if (bound-and-true-p read-extended-command-predicate)
(funcall read-extended-command-predicate sym buffer)
t))
(let ((name (symbol-name sym)))
(unless (string-match-p command-filter name)
(push (propertize name
'consult--candidate sym
'consult--type key)
commands))))))))))))
(defun consult-mode-command (&rest modes)
"Run a command from any of the given MODES.
If no MODES are specified, use currently active major and minor modes."
(interactive)
(unless modes
(setq modes (cons major-mode
(seq-filter (lambda (m)
(and (boundp m) (symbol-value m)))
minor-mode-list))))
(let ((narrow `((?m . ,(format "Major: %s" major-mode))
(?l . "Local Minor")
(?g . "Global Minor"))))
(command-execute
(consult--read
(consult--mode-command-candidates modes)
:prompt "Mode command: "
:predicate
(lambda (cand)
(let ((key (get-text-property 0 'consult--type cand)))
(if consult--narrow
(= key consult--narrow)
(/= key ?g))))
:lookup #'consult--lookup-candidate
:group (consult--type-group narrow)
:narrow narrow
:require-match t
:history 'extended-command-history
:category 'command))))
(defun consult--read-from-kill-ring ()
"Open kill ring menu and return selected string."
(current-kill 0)
(consult--lookup-member
(consult--read
(consult--remove-dups
(or (if consult-yank-rotate
(append kill-ring-yank-pointer
(butlast kill-ring (length kill-ring-yank-pointer)))
kill-ring)
(user-error "Kill ring is empty")))
:prompt "Yank from kill-ring: "
:history t :sort nil
:category 'kill-ring
:require-match t
:state
(consult--insertion-preview
(point)
(or (and (eq last-command 'yank) (mark t)) (point))))
kill-ring))
(defun consult-yank-from-kill-ring (string &optional arg)
"Select STRING from the kill ring and insert it.
With prefix ARG, put point at beginning, and mark at end, like `yank' does.
This command behaves like `yank-from-kill-ring' in Emacs 28, which also offers
a `completing-read' interface to the `kill-ring'. Additionally the Consult
version supports preview of the selected string."
(interactive (list (consult--read-from-kill-ring) current-prefix-arg))
(when string
(setq yank-window-start (window-start))
(push-mark)
(insert-for-yank string)
(setq this-command 'yank)
(when consult-yank-rotate
(if-let (pos (seq-position kill-ring string))
(setq kill-ring-yank-pointer (nthcdr pos kill-ring))
(kill-new string)))
(when (consp arg)
(goto-char (prog1 (mark t)
(set-marker (mark-marker) (point) (current-buffer)))))))
(put 'consult-yank-replace 'delete-selection 'yank)
(put 'consult-yank-pop 'delete-selection 'yank)
(put 'consult-yank-from-kill-ring 'delete-selection 'yank)
(defun consult-yank-pop (&optional arg)
"If there is a recent yank act like `yank-pop'.
Otherwise select string from the kill ring and insert it.
See `yank-pop' for the meaning of ARG.
This command behaves like `yank-pop' in Emacs 28, which also offers a
`completing-read' interface to the `kill-ring'. Additionally the Consult
version supports preview of the selected string."
(interactive "*p")
(if (eq last-command 'yank)
(yank-pop (or arg 1))
(call-interactively #'consult-yank-from-kill-ring)))
(defun consult-yank-replace (string)
"Select STRING from the kill ring.
If there was no recent yank, insert the string.
Otherwise replace the just-yanked string with the selected string.
There exists no equivalent of this command in Emacs 28."
(interactive (list (consult--read-from-kill-ring)))
(when string
(if (not (eq last-command 'yank))
(consult-yank-from-kill-ring string)
(let ((inhibit-read-only t)
(pt (point))
(mk (mark t)))
(setq this-command 'yank)
(funcall (or yank-undo-function 'delete-region) (min pt mk) (max pt mk))
(setq yank-undo-function nil)
(set-marker (mark-marker) pt (current-buffer))
(insert-for-yank string)
(set-window-start (selected-window) yank-window-start t)
(if (< pt mk)
(goto-char (prog1 (mark t)
(set-marker (mark-marker) (point) (current-buffer)))))))))
(defun consult--bookmark-preview ()
"Create preview function for bookmarks."
(let ((preview (consult--jump-preview))
(open (consult--temporary-files)))
(lambda (action cand)
(unless cand
(funcall open))
(funcall
preview action
(when-let ((bm (and cand (eq action 'preview) (assoc cand bookmark-alist)))
(handler (bookmark-get-handler bm))
(file (and (or (not handler)
(eq handler #'bookmark-default-handler))
(bookmark-get-filename bm)))
(pos (bookmark-get-position bm))
(buf (funcall open file)))
(set-marker (make-marker) pos buf))))))
(defun consult--bookmark-action (bm)
"Open BM via `consult--buffer-action'."
(bookmark-jump bm consult--buffer-display))
(consult--define-state bookmark)
(defun consult--bookmark-candidates ()
"Return bookmark candidates."
(bookmark-maybe-load-default-file)
(let ((narrow (mapcar (pcase-lambda (`(,y ,_ ,x)) (cons x y))
consult-bookmark-narrow)))
(mapcar (lambda (cand)
(propertize (car cand)
'consult--type
(alist-get
(or (bookmark-get-handler cand) #'bookmark-default-handler)
narrow)))
bookmark-alist)))
(defun consult-bookmark (name)
"If bookmark NAME exists, open it, otherwise create a new bookmark with NAME.
The command supports preview of file bookmarks and narrowing. See the
variable `consult-bookmark-narrow' for the narrowing configuration."
(interactive
(list
(let ((narrow (mapcar (pcase-lambda (`(,x ,y ,_)) (cons x y))
consult-bookmark-narrow)))
(consult--read
(consult--bookmark-candidates)
:prompt "Bookmark: "
:state (consult--bookmark-preview)
:category 'bookmark
:history 'bookmark-history
:add-history (ignore-errors (bookmark-prop-get (bookmark-make-record) 'defaults))
:group (consult--type-group narrow)
:narrow (consult--type-narrow narrow)))))
(bookmark-maybe-load-default-file)
(if (assoc name bookmark-alist)
(bookmark-jump name)
(bookmark-set name)))
(defun consult-complex-command ()
"Select and evaluate command from the command history.
This command can act as a drop-in replacement for `repeat-complex-command'."
(interactive)
(let* ((history (or (delete-dups (mapcar #'prin1-to-string command-history))
(user-error "There are no previous complex commands")))
(cmd (read (consult--read
history
:prompt "Command: "
:default (car history)
:sort nil
:history t :category 'expression))))
(add-to-history 'command-history cmd)
(apply #'funcall-interactively
(car cmd)
(mapcar (lambda (e) (eval e t)) (cdr cmd)))))
(declare-function ring-elements "ring")
(defun consult--current-history ()
"Return the history and index variable relevant to the current buffer.
If the minibuffer is active, the minibuffer history is returned,
otherwise the history corresponding to the mode. There is a
special case for `repeat-complex-command', for which the command
history is used."
(cond
((minibufferp)
(when (eq minibuffer-history-variable t)
(user-error "Minibuffer history is disabled for `%s'" this-command))
(list (mapcar #'consult--tofu-hide
(if (eq minibuffer-history-variable 'command-history)
(mapcar #'prin1-to-string command-history)
(symbol-value minibuffer-history-variable)))))
(t (let ((found (seq-find (lambda (h)
(and (derived-mode-p (car h))
(boundp (if (consp (cdr h)) (cadr h) (cdr h)))))
consult-mode-histories)))
(unless found
(user-error "No history configured for `%s', see `consult-mode-histories'"
major-mode))
(cons (symbol-value (cadr found)) (cddr found))))))
(defun consult-history (&optional history index bol)
"Insert string from HISTORY of current buffer.
In order to select from a specific HISTORY, pass the history
variable as argument. INDEX is the name of the index variable to
update, if any. BOL is the function which jumps to the beginning
of the prompt. See also `cape-history' from the Cape package."
(interactive)
(pcase-let* ((`(,history ,index ,bol) (if history
(list history index bol)
(consult--current-history)))
(history (if (ring-p history) (ring-elements history) history))
(`(,beg . ,end)
(if (minibufferp)
(cons (minibuffer-prompt-end) (point-max))
(if bol
(save-excursion
(funcall bol)
(cons (point) (pos-eol)))
(cons (point) (point)))))
(str (consult--local-let ((enable-recursive-minibuffers t))
(consult--read
(or (consult--remove-dups history)
(user-error "History is empty"))
:prompt "History: "
:history t :category (and (minibufferp)
(pcase minibuffer-history-variable
('extended-command-history 'command)
('buffer-name-history 'buffer)
('face-name-history 'face)
('read-envvar-name-history 'environment-variable)
('bookmark-history 'bookmark)
('file-name-history 'file)))
:sort nil
:initial (buffer-substring-no-properties beg end)
:state (consult--insertion-preview beg end)))))
(delete-region beg end)
(when index
(set index (seq-position history str)))
(insert (substring-no-properties str))))
(defun consult-isearch-forward (&optional reverse)
"Continue isearch forward optionally in REVERSE."
(interactive)
(consult--require-minibuffer)
(setq isearch-new-forward (not reverse) isearch-new-nonincremental nil)
(funcall (or (command-remapping #'exit-minibuffer) #'exit-minibuffer)))
(defun consult-isearch-backward (&optional reverse)
"Continue isearch backward optionally in REVERSE."
(interactive)
(consult-isearch-forward (not reverse)))
(put #'consult-isearch-backward 'completion-predicate #'ignore)
(put #'consult-isearch-forward 'completion-predicate #'ignore)
(defvar-keymap consult-isearch-history-map
:doc "Additional keymap used by `consult-isearch-history'."
"<remap> <isearch-forward>" #'consult-isearch-forward
"<remap> <isearch-backward>" #'consult-isearch-backward)
(defun consult--isearch-history-candidates ()
"Return isearch history candidates."
(let ((history (if (eq t search-default-mode)
(append regexp-search-ring search-ring)
(append search-ring regexp-search-ring))))
(delete-dups
(mapcar
(lambda (cand)
(let* ((props (plist-member (text-properties-at 0 cand)
'isearch-regexp-function))
(type (pcase (cadr props)
((and 'nil (guard (not props))) ?r)
('nil ?l)
('word-search-regexp ?w)
('isearch-symbol-regexp ?s)
('char-fold-to-regexp ?c)
(_ ?u))))
(consult--tofu-append cand type)))
history))))
(defconst consult--isearch-history-narrow
'((?c . "Char")
(?u . "Custom")
(?l . "Literal")
(?r . "Regexp")
(?s . "Symbol")
(?w . "Word")))
(defun consult-isearch-history ()
"Read a search string with completion from the Isearch history.
This replaces the current search string if Isearch is active, and
starts a new Isearch session otherwise."
(interactive)
(consult--forbid-minibuffer)
(let* ((isearch-message-function #'ignore)
(cursor-in-echo-area t) (candidates (consult--isearch-history-candidates)))
(unless isearch-mode (isearch-mode t))
(with-isearch-suspended
(setq isearch-new-string
(consult--read
candidates
:prompt "I-search: "
:category 'consult-isearch
:history t :sort nil
:initial isearch-string
:keymap consult-isearch-history-map
:annotate
(lambda (cand)
(consult--annotate-align
cand
(alist-get (consult--tofu-get cand) consult--isearch-history-narrow)))
:group
(lambda (cand transform)
(if transform
cand
(alist-get (consult--tofu-get cand) consult--isearch-history-narrow)))
:lookup
(lambda (selected candidates &rest _)
(if-let (found (member selected candidates))
(substring (car found) 0 -1)
selected))
:state
(lambda (action cand)
(when (and (eq action 'preview) cand)
(setq isearch-string cand)
(isearch-update-from-string-properties cand)
(isearch-update)))
:narrow
(list :predicate
(lambda (cand) (= (consult--tofu-get cand) consult--narrow))
:keys consult--isearch-history-narrow))
isearch-new-message
(mapconcat 'isearch-text-char-description isearch-new-string "")))
(unless (plist-member (text-properties-at 0 isearch-string) 'isearch-regexp-function)
(setq isearch-regexp t
isearch-regexp-function nil))))
(defun consult--minor-mode-candidates ()
"Return list of minor-mode candidate strings."
(mapcar
(pcase-lambda (`(,name . ,sym))
(propertize
name
'consult--candidate sym
'consult--minor-mode-narrow
(logior
(ash (if (local-variable-if-set-p sym) ?l ?g) 8)
(if (and (boundp sym) (symbol-value sym)) ?i ?o))
'consult--minor-mode-group
(concat
(if (local-variable-if-set-p sym) "Local " "Global ")
(if (and (boundp sym) (symbol-value sym)) "On" "Off"))))
(nconc
(mapcar (lambda (sym) (cons (symbol-name sym) sym)) minor-mode-list)
(delq nil
(mapcar (pcase-lambda (`(,sym ,lighter))
(when (and lighter (not (equal "" lighter)))
(setq lighter (string-trim (format-mode-line lighter)))
(unless (string-blank-p lighter)
(cons lighter sym))))
minor-mode-alist)))))
(defconst consult--minor-mode-menu-narrow
'((?l . "Local")
(?g . "Global")
(?i . "On")
(?o . "Off")))
(defun consult-minor-mode-menu ()
"Enable or disable minor mode.
This is an alternative to `minor-mode-menu-from-indicator'."
(interactive)
(call-interactively
(consult--read
(consult--minor-mode-candidates)
:prompt "Minor mode: "
:require-match t
:category 'minor-mode
:group
(lambda (cand transform)
(if transform cand (get-text-property 0 'consult--minor-mode-group cand)))
:narrow
(list :predicate
(lambda (cand)
(let ((narrow (get-text-property 0 'consult--minor-mode-narrow cand)))
(or (= (logand narrow 255) consult--narrow)
(= (ash narrow -8) consult--narrow))))
:keys
consult--minor-mode-menu-narrow)
:lookup #'consult--lookup-candidate
:history 'consult--minor-mode-menu-history)))
(defun consult-theme (theme)
"Disable current themes and enable THEME from `consult-themes'.
The command supports previewing the currently selected theme."
(interactive
(list
(let* ((regexp (consult--regexp-filter
(mapcar (lambda (x) (if (stringp x) x (format "\\`%s\\'" x)))
consult-themes)))
(avail-themes (seq-filter
(lambda (x) (string-match-p regexp (symbol-name x)))
(cons 'default (custom-available-themes))))
(saved-theme (car custom-enabled-themes)))
(consult--read
(mapcar #'symbol-name avail-themes)
:prompt "Theme: "
:require-match t
:category 'theme
:history 'consult--theme-history
:lookup (lambda (selected &rest _)
(setq selected (and selected (intern-soft selected)))
(or (and selected (car (memq selected avail-themes)))
saved-theme))
:state (lambda (action theme)
(pcase action
('return (consult-theme (or theme saved-theme)))
((and 'preview (guard theme)) (consult-theme theme))))
:default (symbol-name (or saved-theme 'default))))))
(when (eq theme 'default) (setq theme nil))
(unless (eq theme (car custom-enabled-themes))
(mapc #'disable-theme custom-enabled-themes)
(when theme
(if (custom-theme-p theme)
(enable-theme theme)
(load-theme theme :no-confirm)))))
(defun consult--buffer-sort-alpha (buffers)
"Sort BUFFERS alphabetically, put starred buffers at the end."
(sort buffers
(lambda (x y)
(setq x (buffer-name x) y (buffer-name y))
(let ((a (and (length> x 0) (eq (aref x 0) ?*)))
(b (and (length> y 0) (eq (aref y 0) ?*))))
(if (eq a b)
(string< x y)
(not a))))))
(defun consult--buffer-sort-alpha-current (buffers)
"Sort BUFFERS alphabetically, put current at the beginning."
(let ((buffers (consult--buffer-sort-alpha buffers))
(current (current-buffer)))
(if (memq current buffers)
(cons current (delq current buffers))
buffers)))
(defun consult--buffer-sort-visibility (buffers)
"Sort BUFFERS by visibility."
(let ((hidden)
(current (current-buffer)))
(consult--keep! buffers
(unless (eq it current)
(if (get-buffer-window it 'visible)
it
(push it hidden)
nil)))
(nconc (nreverse hidden) buffers (list (current-buffer)))))
(defun consult--normalize-directory (dir)
"Normalize directory DIR.
DIR can be project, nil or a path."
(cond
((eq dir 'project) (consult--project-root))
(dir (expand-file-name dir))))
(defun consult--buffer-query-prompt (prompt query)
"Buffer query function returning a scope description.
PROMPT is the prompt format string.
QUERY is passed to `consult--buffer-query'."
(let* ((dir (plist-get query :directory))
(ndir (consult--normalize-directory dir))
(buffers (apply #'consult--buffer-query :directory ndir query))
(count (length buffers)))
(cons (format "%s (%d buffer%s%s): " prompt count
(if (= count 1) "" "s")
(cond
((and ndir (eq dir 'project))
(format ", Project %s" (consult--project-name ndir)))
(ndir (concat ", " (consult--abbreviate-file ndir)))
(t "")))
buffers)))
(cl-defun consult--buffer-query (&key sort directory mode as predicate (filter t)
include (exclude consult-buffer-filter))
"Buffer query function.
DIRECTORY can either be project or a path.
SORT can be visibility, alpha or nil.
FILTER can be either t, nil or invert.
EXCLUDE is a list of regexps.
INCLUDE is a list of regexps.
MODE can be a mode or a list of modes to restrict the returned buffers.
PREDICATE is a predicate function.
AS is a conversion function."
(let ((root (consult--normalize-directory directory))
(buffers (buffer-list)))
(when sort
(setq buffers (funcall (intern (format "consult--buffer-sort-%s" sort)) buffers)))
(when (or filter mode as root)
(let ((mode (ensure-list mode))
(exclude-re (consult--regexp-filter exclude))
(include-re (consult--regexp-filter include)))
(consult--keep! buffers
(and
(or (not mode)
(apply #'provided-mode-derived-p
(buffer-local-value 'major-mode it) mode))
(pcase-exhaustive filter
('nil t)
((or 't 'invert)
(eq (eq filter t)
(and
(or (not exclude)
(not (string-match-p exclude-re (buffer-name it))))
(or (not include)
(not (not (string-match-p include-re (buffer-name it)))))))))
(or (not root)
(when-let (dir (buffer-local-value 'default-directory it))
(string-prefix-p root
(if (and (/= 0 (length dir)) (eq (aref dir 0) ?/))
dir
(expand-file-name dir)))))
(or (not predicate) (funcall predicate it))
(if as (funcall as it) it)))))
buffers))
(defun consult--buffer-file-hash ()
"Return hash table of all buffer file names."
(consult--string-hash (consult--buffer-query :as #'buffer-file-name)))
(defun consult--buffer-preview ()
"Buffer preview function."
(let ((orig-buf (current-buffer)) other-win)
(lambda (action cand)
(when (eq action 'preview)
(when (and (eq consult--buffer-display #'switch-to-buffer-other-window)
(not other-win))
(switch-to-buffer-other-window orig-buf)
(setq other-win (selected-window)))
(let ((win (or other-win (selected-window))))
(when (window-live-p win)
(with-selected-window win
(cond
((and cand (get-buffer cand))
(switch-to-buffer cand 'norecord))
((buffer-live-p orig-buf)
(switch-to-buffer orig-buf 'norecord))))))))))
(defun consult--buffer-action (buffer &optional norecord)
"Switch to BUFFER via `consult--buffer-display' function.
If NORECORD is non-nil, do not record the buffer switch in the buffer list."
(funcall consult--buffer-display buffer norecord))
(consult--define-state buffer)
(defvar consult--source-bookmark
`(:name "Bookmark"
:narrow ?m
:category bookmark
:face consult-bookmark
:history bookmark-history
:items ,#'bookmark-all-names
:state ,#'consult--bookmark-state)
"Bookmark candidate source for `consult-buffer'.")
(defvar consult--source-project-buffer
`(:name "Project Buffer"
:narrow (?p . "Project")
:hidden t
:category buffer
:face consult-buffer
:history buffer-name-history
:state ,#'consult--buffer-state
:enabled ,(lambda () consult-project-function)
:items
,(lambda ()
(when-let (root (consult--project-root))
(consult--buffer-query :sort 'visibility
:directory root
:as #'buffer-name))))
"Project buffer candidate source for `consult-buffer'.")
(defvar consult--source-project-recent-file
`(:name "Project File"
:narrow (?p . "Project")
:hidden t
:category file
:face consult-file
:history file-name-history
:state ,#'consult--file-state
:new
,(lambda (file)
(consult--file-action
(expand-file-name file (consult--project-root))))
:enabled
,(lambda ()
(and consult-project-function
recentf-mode))
:items
,(lambda ()
(when-let (root (consult--project-root))
(let ((len (length root))
(ht (consult--buffer-file-hash))
file-name-handler-alist items)
(dolist (file (bound-and-true-p recentf-list) (nreverse items))
(unless (eq (aref file 0) ?/)
(setq file (expand-file-name file)))
(when (and (not (gethash file ht)) (string-prefix-p root file))
(let ((part (substring file len)))
(when (equal part "") (setq part "./"))
(put-text-property 0 1 'multi-category `(file . ,file) part)
(push part items))))))))
"Project file candidate source for `consult-buffer'.")
(defvar consult--source-hidden-buffer
`(:name "Hidden Buffer"
:narrow 32
:hidden t
:category buffer
:face consult-buffer
:history buffer-name-history
:action ,#'consult--buffer-action
:items
,(lambda () (consult--buffer-query :sort 'visibility
:filter 'invert
:as #'buffer-name)))
"Hidden buffer candidate source for `consult-buffer'.")
(defvar consult--source-modified-buffer
`(:name "Modified Buffer"
:narrow ?*
:hidden t
:category buffer
:face consult-buffer
:history buffer-name-history
:state ,#'consult--buffer-state
:items
,(lambda () (consult--buffer-query :sort 'visibility
:as #'buffer-name
:predicate
(lambda (buf)
(and (buffer-modified-p buf)
(buffer-file-name buf))))))
"Modified buffer candidate source for `consult-buffer'.")
(defvar consult--source-buffer
`(:name "Buffer"
:narrow ?b
:category buffer
:face consult-buffer
:history buffer-name-history
:state ,#'consult--buffer-state
:default t
:items
,(lambda () (consult--buffer-query :sort 'visibility
:as #'buffer-name)))
"Buffer candidate source for `consult-buffer'.")
(defun consult--file-register-p (reg)
"Return non-nil if REG is a file register."
(memq (car-safe (cdr reg)) '(file-query file)))
(autoload 'consult-register--candidates "consult-register")
(defvar consult--source-file-register
`(:name "File Register"
:narrow (?r . "Register")
:category file
:state ,#'consult--file-state
:enabled ,(lambda () (seq-some #'consult--file-register-p register-alist))
:items ,(lambda () (consult-register--candidates #'consult--file-register-p)))
"File register source.")
(defvar consult--source-recent-file
`(:name "File"
:narrow ?f
:category file
:face consult-file
:history file-name-history
:state ,#'consult--file-state
:new ,#'consult--file-action
:enabled ,(lambda () recentf-mode)
:items
,(lambda ()
(let ((ht (consult--buffer-file-hash))
file-name-handler-alist items)
(dolist (file (bound-and-true-p recentf-list) (nreverse items))
(unless (eq (aref file 0) ?/)
(setq file (expand-file-name file)))
(unless (gethash file ht)
(push (abbreviate-file-name file) items))))))
"Recent file candidate source for `consult-buffer'.")
(defun consult-buffer (&optional sources)
"Enhanced `switch-to-buffer' command with support for virtual buffers.
The command supports recent files, bookmarks, views and project files as
virtual buffers. Buffers are previewed. Narrowing to buffers (b), files (f),
bookmarks (m) and project files (p) is supported via the corresponding
keys. In order to determine the project-specific files and buffers, the
`consult-project-function' is used. The virtual buffer SOURCES
default to `consult-buffer-sources'. See `consult--multi' for the
configuration of the virtual buffer sources."
(interactive)
(let ((selected (consult--multi (or sources consult-buffer-sources)
:require-match
(confirm-nonexistent-file-or-buffer)
:prompt "Switch to: "
:history 'consult--buffer-history
:sort nil)))
(unless (plist-get (cdr selected) :match)
(consult--buffer-action (car selected)))))
(setq consult-project-buffer-sources
(list
`(:hidden nil :narrow ?b ,@consult--source-project-buffer)
`(:hidden nil :narrow ?f ,@consult--source-project-recent-file)))
(defmacro consult--with-project (&rest body)
"Ensure that BODY is executed with a project root."
`(let ((consult-project-function
(let ((root (or (consult--project-root t) (user-error "No project found")))
(depth (recursion-depth))
(orig consult-project-function))
(lambda (may-prompt)
(if (= depth (recursion-depth))
root
(funcall orig may-prompt))))))
,@body))
(defun consult-project-buffer ()
"Enhanced `project-switch-to-buffer' command with support for virtual buffers.
The command may prompt you for a project directory if it is invoked from
outside a project. See `consult-buffer' for more details."
(interactive)
(consult--with-project
(consult-buffer consult-project-buffer-sources)))
(defun consult-buffer-other-window ()
"Variant of `consult-buffer' which opens in other window."
(interactive)
(let ((consult--buffer-display #'switch-to-buffer-other-window))
(consult-buffer)))
(defun consult-buffer-other-frame ()
"Variant of `consult-buffer' which opens in other frame."
(interactive)
(let ((consult--buffer-display #'switch-to-buffer-other-frame))
(consult-buffer)))
(defun consult--grep-format (async builder)
"Return ASYNC function highlighting grep match results.
BUILDER is the command line builder function."
(let (highlight)
(lambda (action)
(cond
((stringp action)
(setq highlight (cdr (funcall builder action)))
(funcall async action))
((consp action)
(let ((file "") (file-len 0) result)
(save-match-data
(dolist (str action)
(when (and (string-match consult--grep-match-regexp str)
(or (/= (aref str (match-beginning 3)) ?-)
(/= (match-end 0) (length str))))
(unless (and (= file-len (- (match-end 1) (match-beginning 1)))
(eq t (compare-strings
file 0 file-len
str (match-beginning 1) (match-end 1) nil)))
(setq file (match-string 1 str)
file-len (length file)))
(let* ((line (match-string 2 str))
(ctx (= (aref str (match-beginning 3)) ?-))
(sep (if ctx "-" ":"))
(content (substring str (match-end 0)))
(line-len (length line)))
(when (length> content consult-grep-max-columns)
(setq content (substring content 0 consult-grep-max-columns)))
(when highlight
(funcall highlight content))
(setq str (concat file sep line sep content))
(add-text-properties 0 file-len `(face consult-file consult--prefix-group ,file) str)
(put-text-property (1+ file-len) (+ 1 file-len line-len) 'face 'consult-line-number str)
(when ctx
(add-face-text-property (+ 2 file-len line-len) (length str) 'consult-grep-context 'append str))
(push str result)))))
(funcall async (nreverse result))))
(t (funcall async action))))))
(defun consult--grep-position (cand &optional find-file)
"Return the grep position marker for CAND.
FIND-FILE is the file open function, defaulting to `find-file'."
(when cand
(let* ((file-end (next-single-property-change 0 'face cand))
(line-end (next-single-property-change (1+ file-end) 'face cand))
(matches (consult--point-placement cand (1+ line-end) 'consult-grep-context))
(file (substring-no-properties cand 0 file-end))
(line (string-to-number (substring-no-properties cand (+ 1 file-end) line-end))))
(when-let (pos (consult--marker-from-line-column
(funcall (or find-file #'find-file) file)
line (or (car matches) 0)))
(cons pos (cdr matches))))))
(defun consult--grep-state ()
"Grep state function."
(let ((open (consult--temporary-files))
(jump (consult--jump-state)))
(lambda (action cand)
(unless cand
(funcall open))
(funcall jump action (consult--grep-position
cand
(and (not (eq action 'return)) open))))))
(defun consult--grep-exclude-args ()
"Produce grep exclude arguments.
Take the variables `grep-find-ignored-directories' and
`grep-find-ignored-files' into account."
(unless (boundp 'grep-find-ignored-files) (require 'grep))
(nconc (mapcar (lambda (s) (concat "--exclude=" s))
(bound-and-true-p grep-find-ignored-files))
(mapcar (lambda (s) (concat "--exclude-dir=" s))
(bound-and-true-p grep-find-ignored-directories))))
(defun consult--grep (prompt make-builder dir initial)
"Run asynchronous grep.
MAKE-BUILDER is the function that returns the command line
builder function. DIR is a directory or a list of file or
directories. PROMPT is the prompt string. INITIAL is inital
input."
(pcase-let* ((`(,prompt ,paths ,dir) (consult--directory-prompt prompt dir))
(default-directory dir)
(builder (funcall make-builder paths)))
(consult--read
(consult--async-command builder
(consult--grep-format builder)
:file-handler t) :prompt prompt
:lookup #'consult--lookup-member
:state (consult--grep-state)
:initial (consult--async-split-initial initial)
:add-history (consult--async-split-thingatpt 'symbol)
:require-match t
:category 'consult-grep
:group #'consult--prefix-group
:history '(:input consult--grep-history)
:sort nil)))
(defun consult--grep-lookahead-p (&rest cmd)
"Return t if grep CMD supports lookahead."
(with-temp-buffer
(insert "xaxbx")
(eq 0 (apply #'call-process-region (point-min) (point-max)
(car cmd) nil nil nil `(,@(cdr cmd) "^(?=.*b)(?=.*a)")))))
(defun consult--grep-make-builder (paths)
"Build grep command line and grep across PATHS."
(let* ((cmd (consult--build-args consult-grep-args))
(type (if (consult--grep-lookahead-p (car cmd) "-P") 'pcre 'extended)))
(lambda (input)
(pcase-let* ((`(,arg . ,opts) (consult--command-split input))
(flags (append cmd opts))
(ignore-case (or (member "-i" flags) (member "--ignore-case" flags))))
(if (or (member "-F" flags) (member "--fixed-strings" flags))
(cons (append cmd (list "-e" arg) opts paths)
(apply-partially #'consult--highlight-regexps
(list (regexp-quote arg)) ignore-case))
(pcase-let ((`(,re . ,hl) (funcall consult--regexp-compiler arg type ignore-case)))
(when re
(cons (append cmd
(list (if (eq type 'pcre) "-P" "-E") "-e" (consult--join-regexps re type))
opts paths)
hl))))))))
(defun consult-grep (&optional dir initial)
"Search with `grep' for files in DIR where the content matches a regexp.
The initial input is given by the INITIAL argument. DIR can be
nil, a directory string or a list of file/directory paths. If
`consult-grep' is called interactively with a prefix argument,
the user can specify the directories or files to search in.
Multiple directories must be separated by comma in the
minibuffer, since they are read via `completing-read-multiple'.
By default the project directory is used if
`consult-project-function' is defined and returns non-nil.
Otherwise the `default-directory' is searched.
The input string is split, the first part of the string (grep
input) is passed to the asynchronous grep process and the second
part of the string is passed to the completion-style filtering.
The input string is split at a punctuation character, which is
given as the first character of the input string. The format is
similar to Perl-style regular expressions, e.g., /regexp/.
Furthermore command line options can be passed to grep, specified
behind --. The overall prompt input has the form
`#async-input -- grep-opts#filter-string'.
Note that the grep input string is transformed from Emacs regular
expressions to Posix regular expressions. Always enter Emacs
regular expressions at the prompt. `consult-grep' behaves like
builtin Emacs search commands, e.g., Isearch, which take Emacs
regular expressions. Furthermore the asynchronous input split
into words, each word must match separately and in any order.
See `consult--regexp-compiler' for the inner workings. In order
to disable transformations of the grep input, adjust
`consult--regexp-compiler' accordingly.
Here we give a few example inputs:
#alpha beta : Search for alpha and beta in any order.
#alpha.*beta : Search for alpha before beta.
#\\(alpha\\|beta\\) : Search for alpha or beta (Note Emacs syntax!)
#word -- -C3 : Search for word, include 3 lines as context
#first#second : Search for first, quick filter for second.
The symbol at point is added to the future history."
(interactive "P")
(consult--grep "Grep" #'consult--grep-make-builder dir initial))
(defun consult--git-grep-make-builder (paths)
"Create grep command line builder given PATHS."
(let ((cmd (consult--build-args consult-git-grep-args)))
(lambda (input)
(pcase-let* ((`(,arg . ,opts) (consult--command-split input))
(flags (append cmd opts))
(ignore-case (or (member "-i" flags) (member "--ignore-case" flags))))
(if (or (member "-F" flags) (member "--fixed-strings" flags))
(cons (append cmd (list "-e" arg) opts paths)
(apply-partially #'consult--highlight-regexps
(list (regexp-quote arg)) ignore-case))
(pcase-let ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'extended ignore-case)))
(when re
(cons (append cmd
(cdr (mapcan (lambda (x) (list "--and" "-e" x)) re))
opts paths)
hl))))))))
(defun consult-git-grep (&optional dir initial)
"Search with `git grep' for files in DIR with INITIAL input.
See `consult-grep' for details."
(interactive "P")
(consult--grep "Git-grep" #'consult--git-grep-make-builder dir initial))
(defun consult--ripgrep-make-builder (paths)
"Create ripgrep command line builder given PATHS."
(let* ((cmd (consult--build-args consult-ripgrep-args))
(type (if (consult--grep-lookahead-p (car cmd) "-P") 'pcre 'extended)))
(lambda (input)
(pcase-let* ((`(,arg . ,opts) (consult--command-split input))
(flags (append cmd opts))
(ignore-case (if (or (member "-S" flags) (member "--smart-case" flags))
(let (case-fold-search)
(not (string-match-p "[[:upper:]]" arg)))
(or (member "-i" flags) (member "--ignore-case" flags)))))
(if (or (member "-F" flags) (member "--fixed-strings" flags))
(cons (append cmd (list "-e" arg) opts paths)
(apply-partially #'consult--highlight-regexps
(list (regexp-quote arg)) ignore-case))
(pcase-let ((`(,re . ,hl) (funcall consult--regexp-compiler arg type ignore-case)))
(when re
(cons (append cmd (and (eq type 'pcre) '("-P"))
(list "-e" (consult--join-regexps re type))
opts paths)
hl))))))))
(defun consult-ripgrep (&optional dir initial)
"Search with `rg' for files in DIR with INITIAL input.
See `consult-grep' for details."
(interactive "P")
(consult--grep "Ripgrep" #'consult--ripgrep-make-builder dir initial))
(defun consult--find (prompt builder initial)
"Run find command in current directory.
The function returns the selected file.
The filename at point is added to the future history.
BUILDER is the command line builder function.
PROMPT is the prompt.
INITIAL is inital input."
(consult--read
(consult--async-command builder
(consult--async-map (lambda (x) (string-remove-prefix "./" x)))
(consult--async-highlight builder)
:file-handler t) :prompt prompt
:sort nil
:require-match t
:initial (consult--async-split-initial initial)
:add-history (consult--async-split-thingatpt 'filename)
:category 'file
:history '(:input consult--find-history)))
(defun consult--find-make-builder (paths)
"Build find command line, finding across PATHS."
(let* ((cmd (mapcan (lambda (x)
(if (equal x ".") paths (list x)))
(consult--build-args consult-find-args)))
(type (if (eq 0 (call-process-shell-command
(concat (car cmd) " -regextype emacs -version")))
'emacs 'basic)))
(lambda (input)
(pcase-let* ((`(,arg . ,opts) (consult--command-split input))
(`(,re . ,hl) (funcall consult--regexp-compiler arg type t)))
(when re
(cons (append cmd
(cdr (mapcan
(lambda (x)
`("-and" "-iregex"
,(format ".*%s.*"
(replace-regexp-in-string
"\\\\(\\?:" "\\(" x 'fixedcase 'literal))))
re))
opts)
hl))))))
(defun consult-find (&optional dir initial)
"Search for files in DIR matching input regexp given INITIAL input.
See `consult-grep' for details regarding the asynchronous search
and the arguments."
(interactive "P")
(pcase-let* ((`(,prompt ,paths ,dir) (consult--directory-prompt "Find" dir))
(default-directory dir)
(builder (consult--find-make-builder paths)))
(find-file (consult--find prompt builder initial))))
(defun consult--locate-builder (input)
"Build command line from INPUT."
(pcase-let ((`(,arg . ,opts) (consult--command-split input)))
(unless (string-blank-p arg)
(cons (append (consult--build-args consult-locate-args)
(list arg) opts)
(cdr (consult--default-regexp-compiler input 'basic t))))))
(defun consult-locate (&optional initial)
"Search with `locate' for files which match input given INITIAL input.
The input is treated literally such that locate can take advantage of
the locate database index. Regular expressions would often force a slow
linear search through the entire database. The locate process is started
asynchronously, similar to `consult-grep'. See `consult-grep' for more
details regarding the asynchronous search."
(interactive)
(find-file (consult--find "Locate: " #'consult--locate-builder initial)))
(defun consult--man-builder (input)
"Build command line from INPUT."
(pcase-let* ((`(,arg . ,opts) (consult--command-split input))
(`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
(when re
(cons (append (consult--build-args consult-man-args)
(list (consult--join-regexps re 'basic))
opts)
hl))))
(defun consult--man-format (lines)
"Format man candidates from LINES."
(let ((candidates))
(save-match-data
(dolist (str lines)
(when (string-match "\\`\\(.*?\\([^ ]+\\) *(\\([^,)]+\\)[^)]*).*?\\) +- +\\(.*\\)\\'" str)
(let* ((names (match-string 1 str))
(name (match-string 2 str))
(section (match-string 3 str))
(desc (match-string 4 str))
(cand (format "%s - %s" names desc)))
(add-text-properties 0 (length names)
(list 'face 'consult-file
'consult-man (concat section " " name))
cand)
(push cand candidates)))))
(nreverse candidates)))
(defun consult-man (&optional initial)
"Search for man page given INITIAL input.
The input string is not preprocessed and passed literally to the
underlying man commands. The man process is started asynchronously,
similar to `consult-grep'. See `consult-grep' for more details regarding
the asynchronous search."
(interactive)
(man (consult--read
(consult--async-command #'consult--man-builder
(consult--async-transform consult--man-format)
(consult--async-highlight #'consult--man-builder))
:prompt "Manual entry: "
:require-match t
:category 'consult-man
:lookup (apply-partially #'consult--lookup-prop 'consult-man)
:initial (consult--async-split-initial initial)
:add-history (consult--async-split-thingatpt 'symbol)
:history '(:input consult--man-history))))
(define-minor-mode consult-preview-at-point-mode
"Preview minor mode for *Completions* buffers.
When moving around in the *Completions* buffer, the candidate at point is
automatically previewed."
:init-value nil :group 'consult
(if consult-preview-at-point-mode
(add-hook 'post-command-hook #'consult-preview-at-point nil 'local)
(remove-hook 'post-command-hook #'consult-preview-at-point 'local)))
(defun consult-preview-at-point ()
"Preview candidate at point in *Completions* buffer."
(interactive)
(when-let ((win (active-minibuffer-window))
(buf (window-buffer win))
(fun (buffer-local-value 'consult--preview-function buf)))
(funcall fun)))
(defun consult--default-completion-minibuffer-candidate ()
"Return current minibuffer candidate from default completion system or Icomplete."
(when (and (minibufferp)
(eq completing-read-function #'completing-read-default))
(let ((content (minibuffer-contents-no-properties)))
(if (test-completion content
minibuffer-completion-table
minibuffer-completion-predicate)
content
(when-let ((completions (completion-all-sorted-completions)))
(concat
(substring content 0 (or (cdr (last completions)) 0))
(car completions)))))))
(defun consult--default-completion-list-candidate ()
"Return current candidate at point from completions buffer."
(let (beg end)
(when (and
(derived-mode-p 'completion-list-mode)
(cond
((and (not (eobp)) (get-text-property (point) 'mouse-face))
(setq end (point) beg (1+ (point))))
((and (not (bobp)) (get-text-property (1- (point)) 'mouse-face))
(setq end (1- (point)) beg (point)))))
(setq beg (previous-single-property-change beg 'mouse-face)
end (or (next-single-property-change end 'mouse-face) (point-max)))
(or (get-text-property beg 'completion--string)
(buffer-substring-no-properties beg end)))))
(provide 'consult)
(with-eval-after-load 'icomplete (require 'consult-icomplete))
(with-eval-after-load 'vertico (require 'consult-vertico))
(with-eval-after-load 'mct (add-hook 'consult--completion-refresh-hook
'mct--live-completions-refresh))