diff options
| author | Jack Jamison <jackqjamison@gmail.com> | 2026-07-05 01:19:30 -0400 |
|---|---|---|
| committer | Jack Jamison <jackqjamison@gmail.com> | 2026-07-05 01:19:30 -0400 |
| commit | bdf9a71ab7baa2b1de9abcfd5df1a9107a55d141 (patch) | |
| tree | c4524e6c41aa5107211103401dabfaefc81aa882 /.config/emacs/lisp/minadstack | |
| parent | fe3984f541bd32bdfa418afb305b614176b55ca0 (diff) | |
add a bunch of emacs packages HELP
Diffstat (limited to '.config/emacs/lisp/minadstack')
| -rw-r--r-- | .config/emacs/lisp/minadstack/cape.el | 1359 | ||||
| -rw-r--r-- | .config/emacs/lisp/minadstack/consult.el | 5739 | ||||
| -rw-r--r-- | .config/emacs/lisp/minadstack/corfu-history.el | 114 | ||||
| -rw-r--r-- | .config/emacs/lisp/minadstack/corfu.el | 1444 | ||||
| -rw-r--r-- | .config/emacs/lisp/minadstack/marginalia.el | 1461 | ||||
| -rw-r--r-- | .config/emacs/lisp/minadstack/orderless.el | 672 | ||||
| -rw-r--r-- | .config/emacs/lisp/minadstack/vertico-directory.el | 136 | ||||
| -rw-r--r-- | .config/emacs/lisp/minadstack/vertico.el | 736 |
8 files changed, 11661 insertions, 0 deletions
diff --git a/.config/emacs/lisp/minadstack/cape.el b/.config/emacs/lisp/minadstack/cape.el new file mode 100644 index 0000000..c180044 --- /dev/null +++ b/.config/emacs/lisp/minadstack/cape.el @@ -0,0 +1,1359 @@ +;;; cape.el --- Completion At Point Extensions -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2026 Free Software Foundation, Inc. + +;; Author: Daniel Mendler <mail@daniel-mendler.de> +;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> +;; Created: 2021 +;; Version: 2.7 +;; Package-Requires: ((emacs "29.1") (compat "31")) +;; URL: https://github.com/minad/cape +;; Keywords: abbrev, convenience, matching, completion, text + +;; This file is part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Let your completions fly! This package provides additional completion +;; backends in the form of Capfs, see `completion-at-point-functions'. +;; +;; `cape-abbrev': Complete abbreviation (`add-global-abbrev', `add-mode-abbrev'). +;; `cape-dabbrev': Complete word from current buffers. +;; `cape-dict': Complete word from dictionary file. +;; `cape-elisp-block': Complete Elisp in Org or Markdown code block. +;; `cape-elisp-symbol': Complete Elisp symbol. +;; `cape-emoji': Complete Emoji. +;; `cape-file': Complete file name. +;; `cape-history': Complete from Eshell, Comint or minibuffer history. +;; `cape-keyword': Complete programming language keyword. +;; `cape-line': Complete entire line from file. +;; `cape-rfc1345': Complete Unicode char using RFC 1345 mnemonics. +;; `cape-sgml': Complete Unicode char from SGML entity, e.g., &alpha. +;; `cape-tex': Complete Unicode char from TeX command, e.g. \hbar. + +;;; Code: + +(require 'compat) +(eval-when-compile + (require 'cl-lib) + (require 'subr-x)) + +;;;; Customization + +(defgroup cape nil + "Completion At Point Extensions." + :link '(info-link :tag "Info Manual" "(cape)") + :link '(url-link :tag "Website" "https://github.com/minad/cape") + :link '(emacs-library-link :tag "Library Source" "cape.el") + :group 'convenience + :group 'tools + :group 'matching + :prefix "cape-") + +(defcustom cape-dict-limit 100 + "Maximal number of completion candidates returned by `cape-dict'." + :type '(choice (const nil) natnum)) + +;; TODO bug#80071 file-local language. Add mechanism to locate dictionary file +;; based on file-local language variable. +(defcustom cape-dict-file "/usr/share/dict/words" + "Path to dictionary word list file. +This variable can also be a list of paths or +a function returning a single or more paths." + :type '(choice string (repeat string) function)) + +(defcustom cape-dict-case-replace 'case-replace + "Preserve case of input. +See `dabbrev-case-replace' for details." + :type '(choice (const :tag "Disable" nil) + (const :tag "Use `case-replace'" case-replace) + (other :tag "Enable" t))) + +(defcustom cape-dict-case-fold 'case-fold-search + "Case fold search during search. +See `dabbrev-case-fold-search' for details." + :type '(choice (const :tag "Disable" nil) + (const :tag "Use `case-fold-search'" case-fold-search) + (other :tag "Enable" t))) + +(defcustom cape-dabbrev-buffer-function #'cape-same-mode-buffers + "Function which returns list of buffers. +The buffers are scanned for completion candidates by `cape-dabbrev'." + :type `(choice (const :tag "Current buffer" current-buffer) + (const :tag "Text buffers" ,#'cape-text-buffers) + (const :tag "Buffers with same mode" ,#'cape-same-mode-buffers) + (function :tag "Custom function"))) + +(defcustom cape-file-directory nil + "Base directory used by `cape-file." + :type '(choice (const nil) string function)) + +(defcustom cape-file-prefix "file:" + "File completion trigger prefixes. +The value can be a string or a list of strings. The default +`file:' is the prefix of Org file links which work in arbitrary +buffers via `org-open-at-point-global'." + :type '(choice string (repeat string))) + +(defcustom cape-file-directory-must-exist t + "The parent directory must exist for file completion." + :type 'boolean) + +(defcustom cape-line-buffer-function #'cape-same-mode-buffers + "Function which returns list of buffers. +The buffers are scanned for completion candidates by `cape-line'." + :type `(choice (const :tag "Current buffer" current-buffer) + (const :tag "Text buffers" ,#'cape-text-buffers) + (const :tag "Buffers with same mode" ,#'cape-same-mode-buffers) + (function :tag "Custom function"))) + +(defcustom cape-elisp-symbol-wrapper + '((org-mode ?~ ?~) + (markdown-mode ?` ?`) + (emacs-lisp-mode ?` ?') + (rst-mode "``" "``") + (log-edit-mode "`" "'") + (change-log-mode "`" "'") + (message-mode "`" "'") + (rcirc-mode "`" "'")) + "Wrapper characters for symbols." + :type '(alist :key-type symbol :value-type (list (choice character string) + (choice character string)))) + +;;;; Helpers + +(defun cape--buffer-list (pred) + "Return list of buffers satisfying PRED." + (let* ((cur (current-buffer)) + (orig (and (minibufferp) (window-buffer (minibuffer-selected-window)))) + (list (cl-loop for buf in (buffer-list) + if (and (not (eq buf cur)) (not (eq buf orig)) + (funcall pred buf)) + collect buf))) + `(,cur ,@(and orig (list orig)) ,@list))) + +(defun cape-same-mode-buffers () + "Return buffers with same major mode as current buffer." + (cape--buffer-list + (lambda (buf) (eq major-mode (buffer-local-value 'major-mode buf))))) + +(defun cape-text-buffers () + "Return `text-mode' and `prog-mode' buffers." + (cape--buffer-list + (lambda (buf) + (let ((mode (buffer-local-value 'major-mode buf))) + (or (provided-mode-derived-p mode #'text-mode) + (provided-mode-derived-p mode #'prog-mode)))))) + +(defun cape--case-fold-p (fold) + "Return non-nil if case folding is enabled for FOLD." + (if (eq fold 'case-fold-search) case-fold-search fold)) + +(defun cape--case-replace-list (flag input strs) + "Replace case of STRS depending on INPUT and FLAG." + (if (and (if (eq flag 'case-replace) case-replace flag) + (let (case-fold-search) (string-match-p "\\`[[:upper:]]" input))) + (mapcar (apply-partially #'cape--case-replace flag input) strs) + strs)) + +(defun cape--case-replace (flag input str) + "Replace case of STR depending on INPUT and FLAG." + (or (and (if (eq flag 'case-replace) case-replace flag) + (string-prefix-p input str t) + (let (case-fold-search) (string-match-p "\\`[[:upper:]]" input)) + (save-match-data + ;; Ensure that single character uppercase input does not lead to an + ;; all uppercase result. + (when (and (= (length input) 1) (> (length str) 1)) + (setq input (concat input (substring str 1 2)))) + (and (string-match input input) + (replace-match str nil nil input)))) + str)) + +(defun cape--separator-p (str) + "Return non-nil if input STR has a separator character. +Separator characters are used by completion styles like Orderless +to split filter words. In Corfu, the separator is configurable +via the variable `corfu-separator'." + (string-search (string ;; Support `corfu-separator' and Orderless + (or (and (bound-and-true-p corfu-mode) + (bound-and-true-p corfu-separator)) + ?\s)) + str)) + +(defmacro cape--silent (&rest body) + "Silence BODY." + (declare (indent 0)) + `(cl-letf ((inhibit-message t) + (message-log-max nil) + ((symbol-function #'minibuffer-message) #'ignore)) + (ignore-errors ,@body))) + +(defun cape--bounds (thing) + "Return bounds of THING." + (or (bounds-of-thing-at-point thing) (cons (point) (point)))) + +(defmacro cape--wrapped-table (wrap body) + "Create wrapped completion table, handle `completion--unquote'. +WRAP is the wrapper function. +BODY is the wrapping expression." + (declare (indent 1)) + `(lambda (str pred action) + (,@body + (let ((result (complete-with-action action table str pred))) + (when (and (eq action 'completion--unquote) (functionp (cadr result))) + (cl-callf ,wrap (cadr result))) + result)))) + +(defun cape--accept-all-table (table) + "Create completion TABLE which accepts all input." + (cape--wrapped-table cape--accept-all-table + (or (eq action 'lambda)))) + +(defun cape--passthrough-table (table) + "Create completion TABLE disabling any filtering." + (cape--wrapped-table cape--passthrough-table + (let (completion-ignore-case completion-regexp-list (_ (setq str "")))))) + +(defun cape--noninterruptible-table (table) + "Create non-interruptible completion TABLE." + (cape--wrapped-table cape--noninterruptible-table + (let (throw-on-input)))) + +(defun cape--silent-table (table) + "Create a new completion TABLE which is silent (no messages, no errors)." + (cape--wrapped-table cape--silent-table + (cape--silent))) + +(defun cape--nonessential-table (table) + "Mark completion TABLE as `non-essential'." + (let ((dir default-directory)) + (cape--wrapped-table cape--nonessential-table + (let ((default-directory dir) + (non-essential t)))))) + +(defun cape--table-drop-metadata (table keys) + "Create completion TABLE without metadata KEYS." + (if (functionp table) + (lambda (str pred action) + (if (eq action 'metadata) + (when-let* ((md (copy-sequence (funcall table str pred action)))) + (dolist (k keys) (setq md (assq-delete-all k md))) + md) + (complete-with-action action table str pred))) + table)) + +(defvar cape--debug-length 5 + "Length of printed lists in `cape--debug-print'.") + +(defvar cape--debug-id 0 + "Completion table identifier.") + +(defun cape--debug-message (&rest msg) + "Print debug MSG." + (let ((inhibit-message t)) + (apply #'message msg))) + +(defun cape--debug-print (obj &optional full) + "Print OBJ as string, truncate lists if FULL is nil." + (cond + ((symbolp obj) (symbol-name obj)) + ((functionp obj) "#<function>") + ((proper-list-p obj) + (concat + "(" + (string-join + (mapcar #'cape--debug-print + (if full obj (take cape--debug-length obj))) + " ") + (if (and (not full) (length> obj cape--debug-length)) " ...)" ")"))) + (t (let ((print-level 2)) + (prin1-to-string obj))))) + +(defun cape--debug-table (table name beg end) + "Create completion TABLE with debug messages. +NAME is the name of the Capf, BEG and END are the input markers." + (lambda (str pred action) + (let ((result (complete-with-action action table str pred))) + (if (and (eq action 'completion--unquote) (functionp (cadr result))) + ;; See `cape--wrapped-table' + (cl-callf cape--debug-table (cadr result) name beg end) + (cape--debug-message + "%s(action=%S input=%s:%s:%S prefix=%S ignore-case=%S%s%s) => %s" + name + (pcase action + ('nil 'try) + ('t 'all) + ('lambda 'test) + (_ action)) + (+ beg 0) (+ end 0) (buffer-substring-no-properties beg end) + str completion-ignore-case + (if completion-regexp-list + (concat " regexp=" (cape--debug-print completion-regexp-list t)) + "") + (if pred + (concat " predicate=" (cape--debug-print pred)) + "") + (cape--debug-print result))) + result))) + +(defun cape--dynamic-table (beg end fun) + "Create dynamic completion table from FUN with caching. +BEG and END are the input bounds. FUN is the function which +computes the candidates. FUN must return a pair of a predicate +function function and the list of candidates. The predicate is +passed new input and must return non-nil if the candidates are +still valid. + +It is only necessary to use this function if the set of +candidates is computed dynamically based on the input and not +statically determined. The behavior is similar but slightly +different to `completion-table-dynamic'. + +The difference to the builtins `completion-table-dynamic' and +`completion-table-with-cache' is that this function does not use +the prefix argument of the completion table to compute the +candidates. Instead it uses the input in the buffer between BEG +and END to FUN to compute the candidates. This way the dynamic +candidate computation is compatible with non-prefix completion +styles like `substring' or `orderless', which pass the empty +string as first argument to the completion table." + (let ((beg (copy-marker beg)) + (end (copy-marker end t)) + valid table) + (lambda (str pred action) + ;; Bail out early for `metadata' and `boundaries'. This is a pointless + ;; move because of caching, but we do it anyway in the hope that the + ;; profiler report looks less confusing, since the weight of the expensive + ;; FUN computation is moved to the `all-completions' action. Computing + ;; `all-completions' must surely be most expensive, so nobody will suspect + ;; a thing. + (unless (or (eq action 'metadata) (eq (car-safe action) 'boundaries)) + (let ((input (buffer-substring-no-properties beg end))) + (unless (and valid + (or (cape--separator-p input) + (funcall valid input))) + (let* (;; Reset in case `all-completions' is used inside FUN + completion-ignore-case completion-regexp-list + ;; Retrieve new state by calling FUN + (new (and (< beg end) (funcall fun input))) + ;; No interrupt during state update + throw-on-input) + (setq valid (car new) table (cdr new))))) + (complete-with-action action table str pred))))) + +;;;; Capfs + +;;;;; cape-history + +(declare-function ring-elements "ring") +(declare-function eshell-bol "eshell") +(declare-function comint-line-beginning-position "comint") +(defvar eshell-history-ring) +(defvar comint-input-ring) + +(defvar cape--history-properties + (list :company-kind (lambda (_) 'text) + :exclusive 'no + :display-sort-function #'identity + :cycle-sort-function #'identity + :category 'cape-history) + "Completion extra properties for `cape-history'.") + +;;;###autoload +(defun cape-history (&optional interactive) + "Complete from Eshell, Comint or minibuffer history. +See also `consult-history' for a more flexible variant based on +`completing-read'. If INTERACTIVE is nil the function acts like a Capf." + (interactive (list t)) + (if interactive + (cape-interactive #'cape-history) + (let (history bol) + (cond + ((derived-mode-p 'eshell-mode) + (setq history eshell-history-ring + bol (static-if (< emacs-major-version 30) + (save-excursion (eshell-bol) (point)) + (line-beginning-position)))) + ((derived-mode-p 'comint-mode) + (setq history comint-input-ring + bol (comint-line-beginning-position))) + ((and (minibufferp) (not (eq minibuffer-history-variable t))) + (setq history (symbol-value minibuffer-history-variable) + bol (line-beginning-position)))) + (when (ring-p history) + (setq history (ring-elements history))) + (when history + `(,bol ,(point) ,history ,@cape--history-properties))))) + +;;;;; cape-file + +(defvar comint-unquote-function) +(defvar comint-requote-function) + +(defvar cape--file-properties + (list :annotation-function (lambda (s) (if (string-suffix-p "/" s) " Dir" " File")) + :company-kind (lambda (s) (if (string-suffix-p "/" s) 'folder 'file)) + :exclusive 'no + :category 'file) + "Completion extra properties for `cape-file'.") + +;;;###autoload +(defun cape-file (&optional interactive) + "Complete file name at point. +See the user option `cape-file-directory-must-exist'. +If INTERACTIVE is nil the function acts like a Capf." + (interactive (list t)) + (if interactive + (cape-interactive '(cape-file-directory-must-exist) #'cape-file) + (pcase-let* ((default-directory (pcase cape-file-directory + ('nil default-directory) + ((pred stringp) cape-file-directory) + (_ (funcall cape-file-directory)))) + (prefix (and cape-file-prefix + (looking-back + (concat + (regexp-opt (ensure-list cape-file-prefix) t) + "[^ \n\t]*") + (pos-bol)) + (match-end 1))) + (`(,beg . ,end) (if prefix + (cons prefix (point)) + (cape--bounds 'filename))) + (non-essential t) + (file (buffer-substring-no-properties beg end))) + (when (or prefix + (not cape-file-directory-must-exist) + (and (string-search "/" file) + (file-exists-p (file-name-directory + (substitute-in-file-name file))))) + (unless (boundp 'comint-unquote-function) + (require 'comint)) + (let ((table (cape--nonessential-table + (completion-table-with-quoting + #'read-file-name-internal + comint-unquote-function + comint-requote-function)))) + `( ,beg ,end ,table + :company-location + ,(lambda (file) + (let* ((str (buffer-substring-no-properties beg (point))) + (pre (car (completion-boundaries str table nil ""))) + (file (file-name-concat (substring str 0 pre) file))) + (and (file-exists-p file) (list file)))) + ,@(when (or prefix (string-match-p "./" file)) + '(:company-prefix-length t)) + ,@cape--file-properties)))))) + +;;;;; cape-elisp-symbol + +(autoload 'elisp--company-kind "elisp-mode") +(autoload 'elisp--company-doc-buffer "elisp-mode") +(autoload 'elisp--company-doc-string "elisp-mode") +(autoload 'elisp--company-location "elisp-mode") + +(defvar cape--elisp-symbol-properties + (list :annotation-function #'cape--elisp-symbol-annotation + :exit-function #'cape--elisp-symbol-exit + :predicate #'cape--elisp-symbol-predicate + :company-kind #'elisp--company-kind + :company-doc-buffer #'elisp--company-doc-buffer + :company-docsig #'elisp--company-doc-string + :company-location #'elisp--company-location + :exclusive 'no + :category 'symbol) + "Completion extra properties for `cape-elisp-symbol'.") + +(defun cape--elisp-symbol-predicate (sym) + "Return t if SYM is bound, fbound or propertized." + (or (fboundp sym) (boundp sym) (symbol-plist sym))) + +(defun cape--elisp-symbol-exit (sym status) + "Wrap symbol SYM with `cape-elisp-symbol-wrapper' buffers. +STATUS is the exit status." + (when-let* (((not (eq status 'exact))) + (c (cl-loop for (m . c) in cape-elisp-symbol-wrapper + if (derived-mode-p m) return c)) + ((or (not (derived-mode-p 'emacs-lisp-mode)) + ;; Inside comment or string + (let ((s (syntax-ppss))) (or (nth 3 s) (nth 4 s))))) + (x (if (stringp (car c)) (car c) (string (car c)))) + (y (if (stringp (cadr c)) (cadr c) (string (cadr c))))) + (save-excursion + (backward-char (length sym)) + (unless (save-excursion + (and (ignore-errors (or (backward-char (length x)) t)) + (looking-at-p (regexp-quote x)))) + (insert x))) + (unless (looking-at-p (regexp-quote y)) + (insert y)))) + +(defun cape--elisp-symbol-annotation (sym) + "Return kind of SYM." + (setq sym (intern-soft sym)) + (cond + ((special-form-p sym) " Special") + ((macrop sym) " Macro") + ((commandp sym) " Command") + ((fboundp sym) " Function") + ((custom-variable-p sym) " Custom") + ((boundp sym) " Variable") + ((featurep sym) " Feature") + ((facep sym) " Face") + (t " Symbol"))) + +;;;###autoload +(defun cape-elisp-symbol (&optional interactive) + "Complete Elisp symbol at point. +If INTERACTIVE is nil the function acts like a Capf." + (interactive (list t)) + (if interactive + ;; No cycling since it breaks the :exit-function. + (let (completion-cycle-threshold) + (cape-interactive #'cape-elisp-symbol)) + (pcase-let ((`(,beg . ,end) (cape--bounds 'symbol))) + (when (eq (char-after beg) ?') + (setq beg (1+ beg) end (max beg end))) + `(,beg ,end ,obarray ,@cape--elisp-symbol-properties)))) + +;;;;; cape-elisp-block + +(declare-function org-element-context "org-element") +(declare-function markdown-code-block-lang "ext:markdown-mode") + +(defun cape--inside-block-p (&rest langs) + "Return non-nil if inside LANGS code block." + (when-let* ((face (get-text-property (point) 'face)) + (lang (or (and (if (listp face) + (memq 'org-block face) + (eq 'org-block face)) + (plist-get (cadr (org-element-context)) :language)) + (and (if (listp face) + (memq 'markdown-code-face face) + (eq 'markdown-code-face face)) + (save-excursion + (markdown-code-block-lang)))))) + (member lang langs))) + +;;;###autoload +(defun cape-elisp-block (&optional interactive) + "Complete Elisp in Org or Markdown code block. +This Capf is particularly useful for literate Emacs configurations. +If INTERACTIVE is nil the function acts like a Capf." + (interactive (list t)) + (cond + (interactive + ;; No code block check. Always complete Elisp when command was + ;; explicitly invoked interactively. + (cape-interactive #'elisp-completion-at-point)) + ((cape--inside-block-p "elisp" "emacs-lisp") + (elisp-completion-at-point)))) + +;;;;; cape-dabbrev + +(defvar cape--dabbrev-properties + (list :annotation-function (lambda (_) " Dabbrev") + :company-kind (lambda (_) 'text) + :exclusive 'no + :category 'cape-dabbrev) + "Completion extra properties for `cape-dabbrev'.") + +(defvar dabbrev-case-replace) +(defvar dabbrev-case-fold-search) +(defvar dabbrev-abbrev-char-regexp) +(defvar dabbrev-abbrev-skip-leading-regexp) +(declare-function dabbrev--find-all-expansions "dabbrev") +(declare-function dabbrev--reset-global-variables "dabbrev") + +(defun cape--dabbrev-list (input) + "Find all Dabbrev expansions for INPUT." + (cape--silent + (dlet ((dabbrev-check-other-buffers nil) + (dabbrev-check-all-buffers nil) + (dabbrev-backward-only nil) + (dabbrev-limit nil) + (dabbrev-search-these-buffers-only + (ensure-list (funcall cape-dabbrev-buffer-function)))) + (dabbrev--reset-global-variables) + (cons + (apply-partially #'string-prefix-p input) + (cl-loop + with ic = (cape--case-fold-p dabbrev-case-fold-search) + for w in (dabbrev--find-all-expansions input ic) + collect (cape--case-replace (and ic dabbrev-case-replace) input w)))))) + +(defun cape--dabbrev-bounds () + "Return bounds of abbreviation." + (unless (boundp 'dabbrev-abbrev-char-regexp) + (require 'dabbrev)) + (let ((re (or dabbrev-abbrev-char-regexp "\\sw\\|\\s_")) + (limit (minibuffer-prompt-end))) + (if (or (looking-at re) + (and (> (point) limit) + (save-excursion (forward-char -1) (looking-at re)))) + (cons (save-excursion + (while (and (> (point) limit) + (save-excursion (forward-char -1) (looking-at re))) + (forward-char -1)) + (when dabbrev-abbrev-skip-leading-regexp + (while (looking-at dabbrev-abbrev-skip-leading-regexp) + (forward-char 1))) + (point)) + (save-excursion + (while (looking-at re) + (forward-char 1)) + (point))) + (cons (point) (point))))) + +;;;###autoload +(defun cape-dabbrev (&optional interactive) + "Complete with Dabbrev at point. + +If INTERACTIVE is nil the function acts like a Capf. In case you +observe a performance issue with auto-completion and `cape-dabbrev' +it is strongly recommended to disable scanning in other buffers. +See the user option `cape-dabbrev-buffer-function'." + (interactive (list t)) + (if interactive + (cape-interactive #'cape-dabbrev) + (pcase-let ((`(,beg . ,end) (cape--dabbrev-bounds))) + `(,beg ,end + ,(completion-table-case-fold + (cape--dynamic-table beg end #'cape--dabbrev-list) + (not (cape--case-fold-p dabbrev-case-fold-search))) + ,@cape--dabbrev-properties)))) + +;;;;; cape-dict + +(defvar cape--dict-properties + (list :annotation-function (lambda (_) " Dict") + :company-kind (lambda (_) 'text) + :display-sort-function #'identity + :cycle-sort-function #'identity + :exclusive 'no + :category 'cape-dict) + "Completion extra properties for `cape-dict'.") + +(defun cape--dict-list (input) + "Return all words from `cape-dict-file' matching INPUT." + (let* ((inhibit-message t) + (message-log-max nil) + (default-directory + (if (and (not (file-remote-p default-directory)) + (file-directory-p default-directory)) + default-directory + user-emacs-directory)) + (files (mapcar #'expand-file-name + (ensure-list + (if (functionp cape-dict-file) + (funcall cape-dict-file) + cape-dict-file)))) + (words + (apply #'process-lines-ignore-status + "grep" + (concat "-Fh" + (and (cape--case-fold-p cape-dict-case-fold) "i") + (and cape-dict-limit (format "m%d" cape-dict-limit))) + input files))) + (cons + (apply-partially + (if (and cape-dict-limit (length= words cape-dict-limit)) + #'equal #'string-search) + input) + (cape--case-replace-list cape-dict-case-replace input words)))) + +;;;###autoload +(defun cape-dict (&optional interactive) + "Complete word from dictionary at point. +This completion function works best if the dictionary is sorted +by frequency. See the custom option `cape-dict-file'. If +INTERACTIVE is nil the function acts like a Capf." + (interactive (list t)) + (if interactive + (cape-interactive #'cape-dict) + (pcase-let ((`(,beg . ,end) (cape--bounds 'word))) + `( ,beg ,end + ,(completion-table-case-fold + (cape--dynamic-table beg end #'cape--dict-list) + (not (cape--case-fold-p cape-dict-case-fold))) + ,@cape--dict-properties)))) + +;;;;; cape-abbrev + +(defun cape--abbrev-list () + "Abbreviation list." + (delete "" (cl-loop for x in (abbrev--suggest-get-active-tables-including-parents) + nconc (all-completions "" x)))) + +(defun cape--abbrev-annotation (abbrev) + "Annotate ABBREV with expansion." + (concat " " + (truncate-string-to-width + (format + "%s" + (symbol-value + (cl-loop for x in (abbrev--suggest-get-active-tables-including-parents) + thereis (abbrev--symbol abbrev x)))) + 30 0 nil t))) + +(defun cape--abbrev-exit (_str status) + "Expand expansion if STATUS is not exact." + (unless (eq status 'exact) + (expand-abbrev))) + +(defvar cape--abbrev-properties + (list :annotation-function #'cape--abbrev-annotation + :exit-function #'cape--abbrev-exit + :company-kind (lambda (_) 'snippet) + :exclusive 'no + :category 'cape-abbrev) + "Completion extra properties for `cape-abbrev'.") + +;;;###autoload +(defun cape-abbrev (&optional interactive) + "Complete abbreviation at point. +If INTERACTIVE is nil the function acts like a Capf." + (interactive (list t)) + (if interactive + ;; No cycling since it breaks the :exit-function. + (let (completion-cycle-threshold) + (cape-interactive #'cape-abbrev)) + (when-let* ((abbrevs (cape--abbrev-list)) + (bounds (cape--bounds 'symbol))) + `(,(car bounds) ,(cdr bounds) ,abbrevs ,@cape--abbrev-properties)))) + +;;;;; cape-line + +(defvar cape--line-properties + (list :display-sort-function #'identity + :cycle-sort-function #'identity + :exclusive 'no + :category 'cape-line) + "Completion extra properties for `cape-line'.") + +(defun cape--line-list () + "Return all lines from buffer." + (let ((ht (make-hash-table :test #'equal)) + (curr-buf (current-buffer)) + (buffers (funcall cape-line-buffer-function)) + lines) + (dolist (buf (ensure-list buffers)) + (with-current-buffer buf + (let ((beg (point-min)) + (max (point-max)) + (pt (if (eq curr-buf buf) (point) -1)) + end) + (save-excursion + (while (< beg max) + (goto-char beg) + (setq end (pos-eol)) + (unless (<= beg pt end) + (let ((line (buffer-substring-no-properties beg end))) + (unless (or (string-blank-p line) (gethash line ht)) + (puthash line t ht) + (push line lines)))) + (setq beg (1+ end))))))) + (nreverse lines))) + +;;;###autoload +(defun cape-line (&optional interactive) + "Complete current line from other lines. +The buffers returned by `cape-line-buffer-function' are scanned for lines. +If INTERACTIVE is nil the function acts like a Capf." + (interactive (list t)) + (if interactive + (cape-interactive #'cape-line) + `(,(pos-bol) ,(point) ,(cape--line-list) ,@cape--line-properties))) + +;;;; Capf combinators + +(defun cape--company-call (&rest app) + "Apply APP and handle future return values." + ;; Backends are non-interruptible. Disable interrupts! + (let ((toi throw-on-input) + (throw-on-input nil)) + (pcase (apply app) + ;; Handle async future return values. + (`(:async . ,fetch) + (let ((res 'cape--waiting)) + (if toi + (unwind-protect + (progn + (funcall fetch + (lambda (arg) + (when (eq res 'cape--waiting) + (push 'cape--done unread-command-events) + (setq res arg)))) + (when (eq res 'cape--waiting) + (let ((ev (let ((input-method-function nil) + (echo-keystrokes 0)) + (read-event nil t)))) + (unless (eq ev 'cape--done) + (push (cons t ev) unread-command-events) + (setq res 'cape--cancelled) + (throw toi t))))) + (setq unread-command-events + (delq 'cape--done unread-command-events))) + (funcall fetch (lambda (arg) (setq res arg))) + ;; Force synchronization, not interruptible! We use polling + ;; here and ignore pending input since we don't use + ;; `sit-for'. This is the same method used by Company itself. + (while (eq res 'cape--waiting) + (sleep-for 0.01))) + res)) + ;; Plain old synchronous return value. + (res res)))) + +(defvar-local cape--company-init nil) + +;;;###autoload +(defun cape-company-to-capf (backend &optional valid) + "Convert Company BACKEND function to Capf. +VALID is a function taking the old and new input string. It should +return nil if the cached candidates became invalid. The default value +for VALID is `string-prefix-p' such that the candidates are only fetched +again if the input prefix changed." + (lambda () + (when (and (symbolp backend) (not (fboundp backend))) + (ignore-errors (require backend nil t))) + (when (bound-and-true-p company-mode) + (error "`cape-company-to-capf' should not be used with `company-mode', use the Company backend directly instead")) + (when (and (symbolp backend) (not (alist-get backend cape--company-init))) + (funcall backend 'init) + (put backend 'company-init t) + (setf (alist-get backend cape--company-init) t)) + (when-let* ((pre (pcase (cape--company-call backend 'prefix) + ((or `(,p ,_s) (and (pred stringp) p)) (cons p (length p))) + ((or `(,p ,_s ,l) `(,p . ,l)) (cons p l))))) + (let* ((end (point)) (beg (- end (length (car pre)))) + (valid (if (cape--company-call backend 'no-cache (car pre)) + #'equal (or valid #'string-prefix-p))) + (sort-fun (and (cape--company-call backend 'sorted) #'identity)) + restore-props) + (list beg end + (funcall + (if (cape--company-call backend 'ignore-case) + #'completion-table-case-fold + #'identity) + (cape--dynamic-table + beg end + (lambda (input) + (let ((cands (cape--company-call backend 'candidates input))) + ;; The candidate string including text properties should be + ;; restored in the :exit-function, unless the UI guarantees + ;; this itself, like Corfu. + (unless (bound-and-true-p corfu-mode) + (setq restore-props cands)) + (cons (apply-partially valid input) cands))))) + :category backend + :exclusive 'no + :company-prefix-length (cdr pre) + :company-doc-buffer (lambda (x) (cape--company-call backend 'doc-buffer x)) + :company-location (lambda (x) (cape--company-call backend 'location x)) + :company-docsig (lambda (x) (cape--company-call backend 'meta x)) + :company-deprecated (lambda (x) (cape--company-call backend 'deprecated x)) + :company-kind (lambda (x) (cape--company-call backend 'kind x)) + :display-sort-function sort-fun + :cycle-sort-function sort-fun + :annotation-function (lambda (x) + (when-let* ((ann (cape--company-call backend 'annotation x))) + (concat " " (string-trim ann)))) + :exit-function (lambda (x _status) + ;; Restore the candidate string including + ;; properties if restore-props is non-nil. See + ;; the comment above. + (setq x (or (car (member x restore-props)) x)) + (cape--company-call backend 'post-completion x))))))) + +;;;###autoload +(defun cape-interactive (&rest capfs) + "Complete interactively with the given CAPFS." + (let* ((ctx (and (consp (car capfs)) (car capfs))) + (capfs (if ctx (cdr capfs) capfs)) + (completion-at-point-functions + (if ctx + (mapcar (lambda (f) `(lambda () (let ,ctx (funcall ',f)))) capfs) + capfs))) + (unless (completion-at-point) + (user-error "%s: No completions" + (mapconcat (lambda (fun) + (if (symbolp fun) + (symbol-name fun) + "anonymous-capf")) + capfs ", "))))) + +;;;###autoload +(defun cape-capf-interactive (capf) + "Create interactive completion function from CAPF." + (lambda (&optional interactive) + (interactive (list t)) + (if interactive (cape-interactive capf) (funcall capf)))) + +(defvar cape--super-functions + '( :company-docsig :company-location :company-kind + :company-doc-buffer :company-deprecated + :annotation-function :exit-function) + "List of extra functions which are handled by `cape-wrap-super'.") + +;;;###autoload +(defun cape-wrap-super (&rest capfs) + "Call CAPFS and return merged completion result. +The CAPFS list can contain the keyword `:with' to mark the Capfs +afterwards as auxiliary. One of the non-auxiliary Capfs before `:with' +must return non-nil for the super Capf to set in and return a non-nil +result. Such behavior is useful when listing multiple super Capfs in +the `completion-at-point-functions': + + (setq completion-at-point-functions + (list (cape-capf-super \\='elisp-completion-at-point + :with \\='tempel-complete) + (cape-capf-super \\='cape-dabbrev + :with \\='tempel-complete))) + +See the dual `cape-wrap-choose' if you want to try multiple Capfs in +turn." + (when-let* ((results (cl-loop for capf in capfs until (eq capf :with) + for res = (funcall capf) + if res collect (cons t res)))) + (pcase-let* ((results (nconc results + (cl-loop for capf in (cdr (memq :with capfs)) + for res = (funcall capf) + if res collect (cons nil res)))) + (`((,_main ,beg ,end . ,_)) results) + (cand-ht nil) + (tables nil) + (exclusive nil) + (prefix-len nil)) + (cl-loop for (main beg2 end2 table . plist) in results do + ;; Note: `cape-capf-super' currently cannot merge Capfs which + ;; trigger at different beginning positions. In order to support + ;; this, take the smallest BEG value and then normalize all + ;; candidates by prefixing them such that they all start at the + ;; smallest BEG position. + (when (= beg beg2) + (push (list main (plist-get plist :predicate) table + ;; Plist attached to the candidates + (mapcan (lambda (f) + (when-let* ((v (plist-get plist f))) + (list f v))) + cape--super-functions)) + tables) + ;; The resulting merged Capf is exclusive if one of the main + ;; Capfs is exclusive. + (when (and main (not (eq (plist-get plist :exclusive) 'no))) + (setq exclusive t)) + (setq end (max end end2)) + (let ((plen (plist-get plist :company-prefix-length))) + (cond + ((eq plen t) + (setq prefix-len t)) + ((and (not prefix-len) (integerp plen)) + (setq prefix-len plen)) + ((and (integerp prefix-len) (integerp plen)) + (setq prefix-len (max prefix-len plen))))))) + (setq tables (nreverse tables)) + `( ,beg ,end + ,(lambda (str pred action) + (pcase action + ((or `(boundaries . ,_) 'metadata) nil) + ('t ;; all-completions + (let ((ht (make-hash-table :test #'equal)) + (candidates nil)) + (cl-loop for (main table-pred table cand-plist) in tables do + (let* ((pr (if (and table-pred pred) + (lambda (x) (and (funcall table-pred x) (funcall pred x))) + (or table-pred pred))) + (md (completion-metadata "" table pr)) + (sort (or (completion-metadata-get md 'display-sort-function) + #'identity)) + ;; Always compute candidates of the main Capf + ;; tables, which come first in the tables + ;; list. For the :with Capfs only compute + ;; candidates if we've already determined that + ;; main candidates are available. + (cands (when (or main (or exclusive cand-ht candidates)) + (funcall sort (all-completions str table pr))))) + ;; Handle duplicates with a hash table. + (cl-loop + for cand in-ref cands + for dup = (gethash cand ht t) do + (cond + ((eq dup t) + ;; Candidate does not yet exist. + (puthash cand cand-plist ht)) + ((not (equal dup cand-plist)) + ;; Duplicate candidate. Candidate plist is + ;; different, therefore disambiguate the + ;; candidates. + (setf cand (propertize cand 'cape-capf-super + (cons cand cand-plist)))))) + (when cands (push cands candidates)))) + (when (or cand-ht candidates) + (setq candidates (apply #'nconc (nreverse candidates)) + cand-ht ht) + candidates))) + (_ ;; try-completion and test-completion + (cl-loop for (_main table-pred table _cand-plist) in tables thereis + (complete-with-action + action table str + (if (and table-pred pred) + (lambda (x) (and (funcall table-pred x) (funcall pred x))) + (or table-pred pred))))))) + :category cape-super + :company-prefix-length ,prefix-len + :display-sort-function ,#'identity + :cycle-sort-function ,#'identity + ,@(and (not exclusive) '(:exclusive no)) + ,@(mapcan + (lambda (prop) + (list prop + (lambda (cand &rest args) + (if-let* ((ref (get-text-property 0 'cape-capf-super cand))) + (when-let* ((fun (plist-get (cdr ref) prop))) + (apply fun (car ref) args)) + (when-let* ((plist (and cand-ht (gethash cand cand-ht))) + (fun (plist-get plist prop))) + (apply fun cand args)))))) + cape--super-functions))))) + +;;;###autoload +(defun cape-wrap-choose (&rest capfs) + "Call each of CAPFS in turn and return first non-nil result. +Use `cape-wrap-choose' to create a single Capf from multiple Capfs. +Usually you want to add multiple non-exclusive Capfs to the variable +`completion-at-point-functions' directly instead. See the dual +`cape-wrap-super' if you want to merge multiple Capf results." + (cl-loop + for capf in capfs thereis + (pcase (funcall capf) + ((and result `(,beg ,end ,table . ,plist)) + (let* ((str (buffer-substring-no-properties beg end)) + (pt (- (point) beg)) + (pred (plist-get plist :predicate)) + (md (completion-metadata (substring str 0 pt) table pred))) + ;; Treat the Capfs always as non-exclusive. Return the first which + ;; returns non-nil. See also the comment in `corfu--capf-wrapper'. + (and (completion-try-completion str table pred pt md) + result)))))) + +;;;###autoload +(defun cape-wrap-debug (capf &optional name) + "Call CAPF and return a completion table which prints trace messages. +If CAPF is an anonymous lambda, pass the Capf NAME explicitly for +meaningful debugging output." + (unless name + (setq name (if (symbolp capf) capf "capf"))) + (setq name (format "%s@%s" name (incf cape--debug-id))) + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + (let* ((limit (1+ cape--debug-length)) + (pred (plist-get plist :predicate)) + (cands + ;; Reset regexps for `all-completions' + (let (completion-ignore-case completion-regexp-list) + (all-completions + "" table + (lambda (&rest args) + (and (or (not pred) (apply pred args)) (>= (decf limit) 0)))))) + (plist-str "") + (plist-elt plist)) + (while (cdr plist-elt) + (setq plist-str (format "%s %s=%s" plist-str + (substring (symbol-name (car plist-elt)) 1) + (cape--debug-print (cadr plist-elt))) + plist-elt (cddr plist-elt))) + (cape--debug-message + "%s => input=%s:%s:%S table=%s%s" + name (+ beg 0) (+ end 0) (buffer-substring-no-properties beg end) + (cape--debug-print cands) + plist-str)) + `( ,beg ,end + ,(cape--debug-table + table name (copy-marker beg) (copy-marker end t)) + ,@(when-let* ((exit (plist-get plist :exit-function))) + (list :exit-function + (lambda (str status) + (cape--debug-message "%s:exit(status=%s string=%S)" + name status str) + (funcall exit str status)))) + . ,plist)) + (result + (cape--debug-message "%s() => %s (No completion)" + name (cape--debug-print result))))) + +;;;###autoload +(defun cape-wrap-buster (capf &optional valid) + "Call CAPF and return a completion table with cache busting. +This function can be used as an advice around an existing Capf. +The cache is busted when the input changes. The argument VALID +can be a function taking the old and new input string. It should +return nil if the new input requires that the completion table is +refreshed. The default value for VALID is `equal', such that the +completion table is refreshed on every input change." + (setq valid (or valid #'equal)) + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + (setq plist `(:cape--buster t . ,plist)) + `( ,beg ,end + ,(let* ((beg (copy-marker beg)) + (end (copy-marker end t)) + (input (buffer-substring-no-properties beg end))) + (lambda (str pred action) + (let ((new-input (buffer-substring-no-properties beg end))) + (unless (or (not (eq action t)) + (cape--separator-p new-input) + (funcall valid input new-input)) + (pcase + ;; Reset in case `all-completions' is used inside CAPF + (let (completion-ignore-case completion-regexp-list) + (funcall capf)) + ((and `(,new-beg ,new-end ,new-table . ,new-plist) + (guard (and (= beg new-beg) (= end new-end)))) + (let (throw-on-input) ;; No interrupt during state update + (setf table new-table + input new-input + (cddr plist) new-plist)))))) + (complete-with-action action table str pred))) + ,@plist)))) + +;;;###autoload +(defun cape-wrap-passthrough (capf) + "Call CAPF and make sure that no completion style filtering takes place. +This function can be used as an advice around an existing Capf." + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + `(,beg ,end ,(cape--passthrough-table table) ,@plist)))) + +;;;###autoload +(defun cape-wrap-properties (capf &rest properties) + "Call CAPF and add completion PROPERTIES. +Completion properties include :exclusive, :category, +:annotation-function, :affixation-function, :display-sort-function, +:company-kind, :company-doc-buffer, :company-docsig, :company-location, +:company-deprecated and :company-prefix-length." + (let ((keys (cl-loop for (k _) on properties by #'cddr + collect (intern (substring (symbol-name k) 1))))) + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + `( ,beg ,end ,(cape--table-drop-metadata table keys) + ,@properties ,@plist))))) + +;;;###autoload +(defun cape-wrap-nonexclusive (capf) + "Call CAPF and ensure that it is marked as non-exclusive. +This function can be used as an advice around an existing Capf." + (cape-wrap-properties capf :exclusive 'no)) + +;;;###autoload +(defun cape-wrap-sort (capf &optional sort) + "Call CAPF and add SORT function as completion metadata. +If the SORT argument is nil or not given, the completion UI will use +its own default sorting algorithm. This function can be used as an +advice around an existing Capf." + (cape-wrap-properties + capf + :display-sort-function sort + :cycle-sort-function sort)) + +;;;###autoload +(defun cape-wrap-predicate (capf predicate) + "Call CAPF and add an additional candidate PREDICATE. +The PREDICATE is passed the candidate symbol or string." + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + `( ,beg ,end ,table + :predicate + ,(if-let* ((pred (plist-get plist :predicate))) + ;; First argument is key, second is value for hash tables. + ;; The first argument can be a cons cell for alists. Then + ;; the candidate itself is either a string or a symbol. We + ;; normalize the calling convention here such that PREDICATE + ;; always receives a string or a symbol. + (lambda (&rest args) + (when (apply pred args) + (setq args (car args)) + (funcall predicate (if (consp args) (car args) args)))) + (lambda (key &optional _val) + (funcall predicate (if (consp key) (car key) key)))) + ,@plist)))) + +;;;###autoload +(defun cape-wrap-silent (capf) + "Call CAPF and silence it (no messages, no errors). +This function can be used as an advice around an existing Capf." + (pcase (cape--silent (funcall capf)) + (`(,beg ,end ,table . ,plist) + `(,beg ,end ,(cape--silent-table table) ,@plist)))) + +;;;###autoload +(defun cape-wrap-case-fold (capf &optional nofold) + "Call CAPF and return a case-insensitive completion table. +If NOFOLD is non-nil return a case sensitive table instead. This +function can be used as an advice around an existing Capf." + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + `(,beg ,end ,(completion-table-case-fold table nofold) ,@plist)))) + +;;;###autoload +(defun cape-wrap-noninterruptible (capf) + "Call CAPF and return a non-interruptible completion table. +This function can be used as an advice around an existing Capf." + (pcase (let (throw-on-input) (funcall capf)) + (`(,beg ,end ,table . ,plist) + `(,beg ,end ,(cape--noninterruptible-table table) ,@plist)))) + +;;;###autoload +(defun cape-wrap-prefix-length (capf length) + "Call CAPF and ensure that prefix length is greater or equal than LENGTH. +If the prefix is long enough, enforce auto completion." + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + (when (>= (- end beg) length) + `(,beg ,end ,table :company-prefix-length t ,@plist))))) + +;;;###autoload +(defun cape-wrap-inside-faces (capf &rest faces) + "Call CAPF only if inside FACES." + (when-let* (((> (point) (point-min))) + (fs (get-text-property (1- (point)) 'face)) + ((if (listp fs) + (cl-loop for f in fs thereis (memq f faces)) + (memq fs faces)))) + (funcall capf))) + +;;;###autoload +(defun cape-wrap-inside-code (capf) + "Call CAPF only if inside code, not inside a comment or string. +This function can be used as an advice around an existing Capf." + (let ((s (syntax-ppss))) + (and (not (nth 3 s)) (not (nth 4 s)) (funcall capf)))) + +;;;###autoload +(defun cape-wrap-inside-comment (capf) + "Call CAPF only if inside comment. +This function can be used as an advice around an existing Capf." + (and (nth 4 (syntax-ppss)) (funcall capf))) + +;;;###autoload +(defun cape-wrap-inside-string (capf) + "Call CAPF only if inside string. +This function can be used as an advice around an existing Capf." + (and (nth 3 (syntax-ppss)) (funcall capf))) + +;;;###autoload +(defun cape-wrap-accept-all (capf) + "Call CAPF and return a completion table which accepts every input. +This function can be used as an advice around an existing Capf." + (pcase (funcall capf) + (`(,beg ,end ,table . ,plist) + `(,beg ,end ,(cape--accept-all-table table) . ,plist)))) + +(defvar cape--trigger-syntax-table (make-syntax-table (syntax-table)) + "Syntax table used for the trigger character.") + +;;;###autoload +(defun cape-wrap-trigger (capf trigger) + "Ensure that TRIGGER character occurs before point and then call CAPF. +See also `corfu-auto-trigger'. +Example: + (setq corfu-auto-trigger \"/\" + completion-at-point-functions + (list (cape-capf-trigger \\='cape-abbrev ?/)))" + (when-let* ((pos (save-excursion (search-backward (char-to-string trigger) (pos-bol) 'noerror))) + ((save-excursion (not (re-search-backward "\\s-" pos 'noerror))))) + (pcase + ;; Treat the trigger character as punctuation. + (with-syntax-table cape--trigger-syntax-table + (unless (eq (char-syntax trigger) ?.) + (modify-syntax-entry trigger ".")) + (funcall capf)) + (`(,beg ,end ,table . ,plist) + (when (<= pos beg (1+ pos)) + `( ,(1+ pos) ,end ,table + :company-prefix-length t + :exit-function + ,(let ((pos (copy-marker pos)) + (end (copy-marker (1+ pos)))) + (lambda (str status) + (delete-region pos end) + (when-let* ((exit (plist-get plist :exit-function))) + (funcall exit str status)))) + . ,plist)))))) + +(dolist (wrapper (list #'cape-wrap-accept-all #'cape-wrap-buster + #'cape-wrap-case-fold #'cape-wrap-choose + #'cape-wrap-debug #'cape-wrap-inside-code + #'cape-wrap-inside-comment #'cape-wrap-inside-faces + #'cape-wrap-inside-string #'cape-wrap-nonexclusive + #'cape-wrap-noninterruptible #'cape-wrap-passthrough + #'cape-wrap-predicate #'cape-wrap-prefix-length + #'cape-wrap-properties #'cape-wrap-silent + #'cape-wrap-sort #'cape-wrap-super #'cape-wrap-trigger)) + (let ((name (string-remove-prefix "cape-wrap-" (symbol-name wrapper)))) + (defalias (intern (format "cape-capf-%s" name)) + (lambda (capf &rest args) (lambda () (apply wrapper capf args))) + (format "Create a %s Capf from CAPF. +The Capf calls `%s' with CAPF and ARGS as arguments. +See `%s' for documentation." name wrapper wrapper)))) + +;;;###autoload (autoload 'cape-capf-accept-all "cape") +;;;###autoload (autoload 'cape-capf-buster "cape") +;;;###autoload (autoload 'cape-capf-case-fold "cape") +;;;###autoload (autoload 'cape-capf-choose "cape") +;;;###autoload (autoload 'cape-capf-debug "cape") +;;;###autoload (autoload 'cape-capf-inside-code "cape") +;;;###autoload (autoload 'cape-capf-inside-comment "cape") +;;;###autoload (autoload 'cape-capf-inside-faces "cape") +;;;###autoload (autoload 'cape-capf-inside-string "cape") +;;;###autoload (autoload 'cape-capf-nonexclusive "cape") +;;;###autoload (autoload 'cape-capf-noninterruptible "cape") +;;;###autoload (autoload 'cape-capf-passthrough "cape") +;;;###autoload (autoload 'cape-capf-predicate "cape") +;;;###autoload (autoload 'cape-capf-prefix-length "cape") +;;;###autoload (autoload 'cape-capf-properties "cape") +;;;###autoload (autoload 'cape-capf-silent "cape") +;;;###autoload (autoload 'cape-capf-sort "cape") +;;;###autoload (autoload 'cape-capf-super "cape") +;;;###autoload (autoload 'cape-capf-trigger "cape") + +(defvar-keymap cape-prefix-map + :doc "Keymap used as completion entry point. +The keymap should be installed globally under a prefix." + "TAB" #'completion-at-point + "M-TAB" #'completion-at-point + "p" #'completion-at-point + "t" #'complete-tag + "d" #'cape-dabbrev + "h" #'cape-history + "f" #'cape-file + "s" #'cape-elisp-symbol + "e" #'cape-elisp-block + "a" #'cape-abbrev + "l" #'cape-line + "w" #'cape-dict + "k" 'cape-keyword + ":" 'cape-emoji + "\\" 'cape-tex + "_" 'cape-tex + "^" 'cape-tex + "&" 'cape-sgml + "r" 'cape-rfc1345) + +;;;###autoload (autoload 'cape-prefix-map "cape" nil t 'keymap) +(defalias 'cape-prefix-map cape-prefix-map) + +(provide 'cape) +;;; cape.el ends here diff --git a/.config/emacs/lisp/minadstack/consult.el b/.config/emacs/lisp/minadstack/consult.el new file mode 100644 index 0000000..d500c9c --- /dev/null +++ b/.config/emacs/lisp/minadstack/consult.el @@ -0,0 +1,5739 @@ +;;; consult.el --- Search and navigate via completing-read -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2026 Free Software Foundation, Inc. + +;; Author: Daniel Mendler and Consult contributors +;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> +;; Created: 2020 +;; Version: 3.6 +;; Package-Requires: ((emacs "29.1") (compat "31")) +;; URL: https://github.com/minad/consult +;; Keywords: matching, files, completion + +;; This file is part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Consult implements a set of `consult-<thing>' commands, which aim to +;; improve the way you use Emacs. The commands are founded on +;; `completing-read', which selects from a list of candidate strings. +;; Consult provides an enhanced buffer switcher `consult-buffer' and +;; search and navigation commands like `consult-imenu' and +;; `consult-line'. Searching through multiple files is supported by the +;; asynchronous `consult-grep' command. Many Consult commands support +;; previewing candidates. If a candidate is selected in the completion +;; view, the buffer shows the candidate immediately. + +;; The Consult commands are compatible with multiple completion systems +;; based on the Emacs `completing-read' API, including the default +;; completion system, Vertico, Mct and Icomplete. + +;; See the README for an overview of the available Consult commands and +;; the documentation of the configuration and installation of the +;; package. + +;; The full list of contributors can be found in the acknowledgments +;; section of the README. + +;;; Code: + +(eval-when-compile + (require 'cl-lib) + (require 'subr-x)) +(require 'compat) +(require 'bookmark) + +(defgroup consult nil + "Search and navigate via `completing-read'." + :link '(info-link :tag "Info Manual" "(consult)") + :link '(url-link :tag "Website" "https://github.com/minad/consult") + :link '(url-link :tag "Wiki" "https://github.com/minad/consult/wiki") + :link '(emacs-library-link :tag "Library Source" "consult.el") + :group 'files + :group 'outlines + :group 'minibuffer + :prefix "consult-") + +;;;; Customization + +(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 key (const :tag "None" 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 key (const :tag "None" nil))) + +(defcustom consult-project-function + #'consult--default-project-function + "Function which returns project root directory. +The function takes one boolean argument 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 + (const :tag "Default project function" ,#'consult--default-project-function) + (function :tag "Custom function") + (const :tag "No project integration" 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 :tag "Delay in seconds")) + +(defcustom consult-async-input-throttle 0.5 + "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 :tag "Delay in seconds")) + +(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 :tag "Delay in seconds")) + +(defcustom consult-async-min-input 3 + "Minimum number of characters needed, before asynchronous process is called. + +This applies to asynchronous commands, e.g., `consult-grep'." + :type '(natnum :tag "Number of characters")) + +(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 + `((none :function ,#'consult--split-none) + (comma :separator ?, :function ,#'consult--split-separator) + (semicolon :separator ?\; :function ,#'consult--split-separator) + (perl :initial ?# :function ,#'consult--split-perl)) + "Async splitting styles." + :type '(alist :key-type symbol :value-type plist)) + +(defcustom consult-async-indicator + '((running ?* consult-async-running) + (finished ?: consult-async-finished) + (killed ?\; consult-async-failed) + (failed ?! consult-async-failed)) + "Async indicator characters and faces. +Set to nil to disable." + :type '(alist :key-type symbol :value-type (list character face))) + +(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 (list #'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'. 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 (* 1024 1024) + "Avoid whole-buffer fontification for buffers larger than this character limit. +This setting affects the command `consult-keep-lines'." + :type '(natnum :tag "Buffer size in characters")) + +(defcustom consult-buffer-filter + '("\\` " + "\\`\\*Completions\\*\\'" + "\\`\\*Multiple Choice Help\\*\\'" + "\\`\\*Flymake log\\*\\'" + "\\`\\*Semantic SymRef\\*\\'" + "\\`\\*vc\\*\\'" + "\\`newsrc-dribble\\'" ;; Gnus + "\\`\\*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. The regular expressions are matched case sensitively." + :type '(repeat regexp)) + +(defcustom consult-buffer-list-function #'buffer-list + "List of buffers to use for selection. +By default, the variable is set to the function `buffer-list', which +returns all buffers from all frames. Set it to +`consult--frame-buffer-list' to only use buffers belonging to the +current frame (or tab-bar tab). Alternatively use a custom function for +custom buffer isolation." + :type `(choice (const :tag "All buffers" ,#'buffer-list) + (const :tag "Frame/Tab buffers" ,#'consult--frame-buffer-list) + (function :tag "Custom function"))) + +(defcustom consult-buffer-sources + '(consult-source-buffer + consult-source-hidden-buffer + consult-source-modified-buffer + consult-source-other-buffer + consult-source-recent-file + consult-source-buffer-register + consult-source-file-register + consult-source-bookmark + consult-source-project-buffer-hidden + consult-source-project-recent-file-hidden + consult-source-project-root-hidden) + "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 + '(consult-source-project-buffer + consult-source-project-recent-file + consult-source-project-root) + "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 + '(;; Filter commands + "-mode\\'" "--" + ;; Filter whole features + simple mwheel time so-long recentf tab-bar tab-line) + "Filter commands for `consult-mode-command'." + :type '(repeat (choice symbol regexp))) + +(defcustom consult-grep-max-columns 300 + "Maximal number of columns of grep output. +If set to nil, do not truncate candidates. This can have negative +performance implications but helps if you want to export long lines via +`embark-export'." + :type '(choice natnum (const nil))) + +(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 sexp)))) + +(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 sexp)))) + +(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 sexp)))) + +(defcustom consult-find-args + "find . -not ( -path */.[A-Za-z]* -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 sexp)))) + +(defcustom consult-fd-args + '((if (executable-find "fdfind" 'remote) "fdfind" "fd") + "--full-path --color=never") + "Command line arguments for fd, see `consult-fd'. +The dynamically computed arguments are appended. +Can be either a string, or a list of strings or expressions." + :type '(choice string (repeat (choice string sexp)))) + +(defcustom consult-locate-args + "locate --ignore-case" ;; --existing not supported by Debian plocate + "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 sexp)))) + +(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 sexp)))) + +(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) + (key :tag "Key") + (repeat :tag "List of keys" key))) + +(defcustom consult-preview-partial-size (* 1024 1024) + "Files larger than this byte limit are previewed partially." + :type '(natnum :tag "File size in bytes")) + +(defcustom consult-preview-partial-chunk (* 10 1024) + "Partial preview chunk size in bytes. +If a file is larger than `consult-preview-partial-size' only the +chunk from the beginning of the file is previewed." + :type '(natnum :tag "Chunk size in bytes")) + +(defcustom consult-preview-max-count 10 + "Number of file buffers to keep open temporarily during preview." + :type '(natnum :tag "Number of buffers")) + +(defcustom consult-preview-excluded-buffers nil + "Buffers excluded from preview. +The value should conform to the predicate format demanded by the +function `buffer-match-p'." + :type 'sexp) + +(defcustom consult-preview-excluded-files + ;; Do not preview remote and gpg files + '("\\`/[^/|:]+:" "\\.gpg\\'") + "List of regexps matched against names of files, which are not previewed." + :type '(repeat regexp)) + +(defcustom consult-preview-allowed-hooks + '(global-font-lock-mode + save-place-find-file-hook) + "List of hooks, which should be executed during file preview. +This variable applies to `find-file-hook', `change-major-mode-hook' and +mode hooks, e.g., `prog-mode-hook'." + :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 Info-bookmark-jump + Man-bookmark-jump woman-bookmark-jump) + (?p "Picture" image-bookmark-jump) + (?d "Docview" doc-view-bookmark-jump) + (?m "Mail" gnus-summary-bookmark-jump) + (?s "Shell" eshell-bookmark-jump shell-bookmark-jump) + (?w "Web" eww-bookmark-jump xwidget-webkit-bookmark-jump-handler) + (?v "VC Directory" vc-dir-bookmark-jump) + (nil "Other")) + "Bookmark narrowing configuration. + +Each element of the list must have the form (char name handlers...)." + :type '(alist :key-type character :value-type (cons string (repeat function)))) + +;;;; Faces + +(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 by `consult-grep'.") + +(defface consult-highlight-mark + '((t :inherit consult-highlight-match)) + "Face used for mark positions in completion candidates. +Used for example by `consult-mark'. The face should be different +than the `cursor' face to avoid confusion.") + +(defface consult-preview-match + '((t :inherit isearch)) + "Face used for match previews, e.g., in `consult-line'.") + +(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 :weight normal)) + "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-async-option + '((t :inherit warning :weight normal)) + "Face used to highlight asynchronous command options.") + +(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 warning :weight normal)) + "Face used to highlight line number prefixes after wrap around.") + +;;;; Input history variables + +(defvar consult--path-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) + +;;;; Internal variables + +(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 + ;; Disable preview in frames, since `consult--jump-preview' does not properly + ;; clean up. See gh:minad/consult#593. This issue should better be fixed in + ;; `consult--jump-preview'. + `((,#'consult-buffer-other-frame :preview-key nil) + (,#'consult-buffer-other-tab :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-list-candidate + #'consult--default-completion-minibuffer-candidate) + "Get candidate from completion system.") + +;; Redisplay such that the updated completion UI will be displayed, even when +;; the update happened due to `accept-process-output' inside a loop of a dynamic +;; collection. See `consult--async-dynamic'. +(defvar consult--completion-refresh-hook + (list #'redisplay #'consult--default-completion-list-refresh) + "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 #x100000 + "Special character used to encode line suffixes for disambiguation. +We use characters in the Unicode PUA-B.") + +(defconst consult--tofu-range #xFFFE + "Special character range.") + +(defconst consult--tofu-regexp + (format "[%c-%c]" consult--tofu-char + (+ consult--tofu-char consult--tofu-range -1)) + "Special character regexp.") + +(defvar-local consult--narrow nil + "Current narrow key.") + +(defvar-local consult--narrow-config nil + "Narrowing config 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.2 + "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 consult--focus-lines-indicator + (propertize + "FOCUS" 'face 'highlight + 'help-echo + "`consult-focus-lines': \\`mouse-1' or \\[consult-focus-lines] \\`RET' to reveal." + 'local-map + (define-keymap "<mode-line> <down-mouse-1>" + (lambda () (interactive) (consult-focus-lines nil 'reveal)))) + "Mode line indicator displayed if `consult-focus-lines' is active.") + +;;;; Miscellaneous helper functions + +(defun consult--plist-remove (keys plist) + "Remove list of KEYS from PLIST." + (let (result) + (while plist + (unless (memq (car plist) keys) + (push (car plist) result) + (push (cadr plist) result)) + (setq plist (cddr plist))) + (nreverse result))) + +(defun consult--key-parse (key) + "Parse KEY or signal error if invalid." + (unless (key-valid-p key) + (error "%S is not a valid key definition; see `key-valid-p'" key)) + (key-parse key)) + +(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) + (let ((result (funcall table str pred action))) + (pcase action + ('metadata + (setq result + (mapcar + (lambda (x) + (if (and (string-suffix-p "-function" (symbol-name (car-safe x))) (cdr x)) + (cons (car x) (consult--in-buffer (cdr x))) + x)) + result))) + ((and 'completion--unquote (guard (functionp (cadr result)))) + (cl-callf consult--in-buffer (cadr result) buffer) + (cl-callf consult--in-buffer (cadddr result) buffer))) + result)) + 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." + (seq-mapcat (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 "")) + (setq str (substring-no-properties str)) + ;; Find first option + (when (string-match "\\(?:\\`\\| \\)-" str) + (setq opts (substring str (1- (match-end 0))) + str (substring str 0 (match-beginning 0))) + (when (equal opts "-") + (setq opts ""))) + ;; Replace backslash-escaped dashes + (setq str (replace-regexp-in-string "\\(\\`\\| \\)\\\\-" "\\1-" str)) + ;; Options end with double dash + (when (string-match "\\(\\`\\| \\)--\\(?: \\|\\'\\)" opts) + (setq str (concat str " " (substring opts (match-end 0))) + opts (substring opts 0 (match-beginning 0)))) + ;; Use `split-string-shell-command' here instead of + ;; `split-string-and-unquote' since it handles more flexible input - + ;; double quoted strings, single quoted strings and escaped spaces. + (cons str (split-string-shell-command (string-trim opts)))))) + +(defmacro consult--keep! (list form) + "Evaluate FORM for every element of LIST and keep the non-nil results." + (declare (indent 1) (debug (gv-place body))) + (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)))) + (setf ,list (cdr ,head)) + nil))) + +(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." + ;; Ensure that the global completion style settings are used for + ;; `consult-line', `consult-focus-lines' and `consult-keep-lines' filtering. + ;; This override is necessary since users may want to override the settings + ;; buffer-locally for in-buffer completion via Corfu. + (dlet ((completion-lazy-hilit (not highlight)) + (completion-styles (default-value 'completion-styles)) + (completion-category-defaults (default-value 'completion-category-defaults)) + (completion-category-overrides (default-value 'completion-category-overrides))) + ;; `completion-all-completions' returns an improper list where the last link + ;; is not necessarily nil. + (nconc (completion-all-completions pattern cands nil (length pattern) + `(metadata (category . ,category))) + nil))) + +(defun consult--completion-filter-complement (pattern cands category) + "Filter CANDS with complement of PATTERN given completion CATEGORY." + (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) ;; empty pattern + ((string-prefix-p "! " pattern) (consult--completion-filter-complement + (substring pattern 2) cands category)) + (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) (debug (symbolp symbolp body))) + (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 (string-width string pos nexti)))) + (setq pos nexti)))))) + width)) + +(defun consult--string-hash (strings) + "Create hash table 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) (debug let)) + (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))))))) + +(defvar consult--fast-abbreviate-file-name nil) +(defun consult--fast-abbreviate-file-name (name) + "Return abbreviate file NAME. +This function is a pure variant of `abbreviate-file-name', which +does not access the file system. This is important if we require +that the operation is fast, even for remote paths or paths on +network file systems." + (save-match-data + (let (case-fold-search) ;; Assume that file system is case sensitive. + (setq name (directory-abbrev-apply name)) + (if (string-match (with-memoization consult--fast-abbreviate-file-name + (directory-abbrev-make-regexp (expand-file-name "~"))) + name) + (concat "~" (substring name (match-beginning 1))) + name)))) + +(defun consult--left-truncate-file (file) + "Return abbreviated file name of FILE for use in `completing-read' prompt." + (save-match-data + (let* ((file-name-handler-alist) ;; No Tramp interference please. + (file (directory-file-name (abbreviate-file-name file))) + (prefix nil)) + (when (string-match "\\`/\\([^/|:]+:[^/|:]*:\\)" file) + (setq prefix (propertize (match-string 1 file) 'face 'error) + file (if (= (match-end 0) (length file)) "/" (substring file (match-end 0))))) + (when (string-match "/\\([^/]+\\)/\\([^/]+\\)\\'" file) + (let* ((fst (truncate-string-to-width (match-string 1 file) 20 nil nil "…")) + (snd (truncate-string-to-width (match-string 2 file) 20 nil nil "…")) + (trunc (format "…/%s/%s" fst snd))) + (setq file (if (< (length trunc) (length file)) trunc file)))) + (concat prefix file)))) + +(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) + ((or 'nil '(16)) (or (consult--project-root dir) default-directory)) + (_ + (pcase (if (stringp (car-safe dir)) + dir + ;; Preserve this-command across `completing-read-multiple' call, + ;; such that `consult-customize' continues to work. + (let ((this-command this-command) + (def (abbreviate-file-name default-directory)) + ;; bug#75910: category instead of `minibuffer-completing-file-name' + (minibuffer-completing-file-name t) + (ignore-case read-file-name-completion-ignore-case)) + (minibuffer-with-setup-hook + (lambda () + (setq-local completion-ignore-case ignore-case) + (set-syntax-table minibuffer-local-filename-syntax)) + (completing-read-multiple "Dirs or files: " + #'completion-file-name-table + nil t def 'consult--path-history def)))) + ((and `(,p) (guard (file-directory-p p))) p) + (ps (setq paths (mapcar (lambda (p) + (file-relative-name (expand-file-name p))) + ps)) + default-directory))))) + (edir (file-name-as-directory (expand-file-name dir))) + (pdir (let ((default-directory edir)) + ;; Bind default-directory in order to find the project + (consult--project-root)))) + (list + (format "%s (%s): " prompt + (pcase paths + ((guard (<= 1 (length paths) 2)) + (string-join (mapcar #'consult--left-truncate-file paths) ", ")) + (`(,p . ,_) + (format "%d paths, %s, …" (length paths) (consult--left-truncate-file p))) + ((guard (equal edir pdir)) (concat "Project " (consult--project-name pdir))) + (_ (consult--left-truncate-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." + (declare-function project-root "project") + (when-let* ((proj (project-current may-prompt))) + (project-root 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." + ;; Preserve this-command across project selection, + ;; such that `consult-customize' continues to work. + (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-known-roots () + "Return list of known project roots." + (let ((root (consult--project-root)) + (dirs (sort (project-known-project-roots) #'string<))) + (when root + (setq root (abbreviate-file-name root) + dirs (cons root (delete root dirs)))) + dirs)) + +(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--original-window () + "Return window which was just selected just before the minibuffer was entered. +In contrast to `minibuffer-selected-window' never return nil and +always return an appropriate non-minibuffer window." + (or (minibuffer-selected-window) + (if (window-minibuffer-p (selected-window)) + (next-window) + (selected-window)))) + +(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))) + +(defsubst 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." + (declare (indent 0) (debug t)) + (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) (debug t)) + `(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) + (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 + (without-restriction + (goto-char (point-min)) + ;; Location data might be invalid by now! + (ignore-errors + (forward-line (1- line)) + (goto-char (min (+ (point) column) (pos-eol)))) + (point-marker)))))) + +(defsubst consult--copy-property (beg end str prop) + "Copy PROP from buffer region BEG to END to STR. +The string STR is modified." + (let ((pos beg)) + (while (< pos end) + (let ((next (next-single-property-change pos prop nil end)) + (val (get-text-property pos prop))) + (when val + (if (eq prop 'face) + (add-face-text-property (- pos beg) (- next beg) val t str) + (put-text-property (- pos beg) (- next beg) prop val str))) + (setq pos next))))) + +(defun consult--copy-faces (beg end str) + "Copy faces from buffer region BEG to END to STR. +The string STR is modified." + (consult--copy-property beg end str 'face) + (consult--copy-property beg end str 'invisible) + (consult--copy-property beg end str 'display)) + +(defun consult--line-fontify (&optional curr-line) + "Annotation function to fontify `consult-location' line and add line number. +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 (propertize before 'face 'consult-line-number-prefix))) + (lambda (cand) + (pcase-let* ((`(,pos . ,line) (get-text-property 0 'consult-location cand)) + (buf (when consult-fontify-preserve + (if (consp pos) + (car pos) + (and (markerp pos) (marker-buffer pos)))))) + (when (buffer-live-p buf) + (with-current-buffer buf + (goto-char (if (markerp pos) pos (cdr pos))) + (let ((beg (pos-bol)) + (end (pos-eol))) + ;; Only apply lazy highlighting if the buffer has not been changed. + (when (string-prefix-p (buffer-substring-no-properties beg end) cand) + (setq cand (copy-sequence cand)) + (consult--fontify-region beg end) + (consult--copy-faces beg end 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) + +(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 (buffer-substring-no-properties beg end))) + (when fontify (consult--fontify-region beg end)) + (consult--copy-faces beg end str) + str) + (buffer-substring-no-properties beg end))) + +(defun consult--line-with-mark (marker) + "Current line string where the MARKER position is highlighted." + (let* ((beg (pos-bol)) + (end (pos-eol)) + (str (consult--buffer-substring beg end 'fontify))) + (if (>= marker end) + (concat str #(" " 0 1 (face consult-highlight-mark))) + (put-text-property (- marker beg) (- (1+ marker) beg) + 'face 'consult-highlight-mark str) + str))) + +;;;; Tofu cooks + +(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-strip (str) + "Strip tofus from STR." + (replace-regexp-in-string consult--tofu-regexp "" (substring-no-properties 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)) + +;; We must disambiguate the lines by adding a suffix such that two lines with +;; the same text can be distinguished. In order to avoid matching the line +;; number, such that the user can search for numbers with `consult-line', we +;; encode the line number as Unicode PUA-B characters. This way accidental +;; matching is unlikely. +(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)) + +;;;; Regexp utilities + +(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) + (decf (car match) pos) + (decf (cdr match) pos)) + (cons pos matches))) + +(defun consult--highlight-regexps (regexps ignore-case str) + "Highlight REGEXPS (or single regexp string) 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 (ensure-list regexps)) + (let ((i 0)) + (while (and (let ((case-fold-search ignore-case)) + (string-match re str i)) + ;; Ensure that regexp search made progress (edge case for .*) + (> (match-end 0) i)) + ;; Unfortunately there is no way to avoid the allocation of the match + ;; data, since the number of capturing groups is unknown. + (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) + +(defun consult--highlight-literals (literals ignore-case str) + "Highlight list of LITERALS or single literal string in STR. +Case insensitive if IGNORE-CASE is non-nil." + (consult--highlight-regexps (mapcar #'regexp-quote (ensure-list literals)) + ignore-case str)) + +(defconst consult--convert-regexp-table + (append + ;; For simplicity, treat word beginning/end as word boundaries, + ;; since PCRE does not make this distinction. Usually the + ;; context determines if \b is the beginning or the end. + '(("\\<" . "\\b") ("\\>" . "\\b") + ("\\_<" . "\\b") ("\\_>" . "\\b") + ("\\s-" . "[ \\n\\t\\r]") ("\\S-" . "[^ \\n\\t\\r]") + ("\\sw" . "[a-zA-Z0-9]") ("\\Sw" . "[^a-zA-Z0-0]") + ("\\s_" . "[a-zA-Z0-9_-]") ("\\S_" . "[^a-zA-Z0-0_-]")) + ;; Treat \` and \' as beginning and end of line. This is more + ;; widely supported and makes sense for line-based commands. + '(("\\`" . "^") ("\\'" . "$")) + ;; Historical: Unescaped *, +, ? are supported at the beginning + (mapcan (lambda (x) + (mapcar (lambda (y) + (cons (concat x y) + (concat (string-remove-prefix "\\" x) "\\" y))) + '("*" "+" "?"))) + '("" "\\(" "\\(?:" "\\|" "^")) + ;; Different escaping + (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 + ;; Support for Emacs regular expressions is fairly complete for basic + ;; usage. There are a few unsupported Emacs regexp features: + ;; - \= point matching + ;; - Most syntax classes \sx \Sx + ;; - Character classes \cx \Cx + ;; - Explicitly numbered groups (?3:group) + (replace-regexp-in-string + (rx (or "\\\\" "\\^" ;; Pass through + (seq (or "\\(?:" "\\|") (any "*+?")) ;; Historical: \|+ or \(?:* etc + (seq "\\(" (any "*+")) ;; Historical: \(* or \(+ + (seq (or bos "^") (any "*+?")) ;; Historical: + or * at the beginning + (seq (opt "\\") (any "(){|}")) ;; Escape parens/braces/pipe + (seq "\\" (any "'<>`")) ;; Special escapes + (seq "\\" (any "Ss") (any "-w_")) ;; Whitespace, word, symbol syntax class + (seq "\\_" (any "<>")))) ;; Beginning or end of symbol + (lambda (x) (or (cdr (assoc x consult--convert-regexp-table)) x)) + regexp 'fixedcase 'literal))) + +(defun consult--default-regexp-compiler (input type ignore-case) + "Compile a string to a list of regular expressions. +See `consult--compile-regexp' for INPUT, TYPE and IGNORE-CASE." + (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--compile-regexp (input type ignore-case) + "Compile the INPUT string to a list of regular expressions. +Return a pair, the list of regular expressions and a highlight function. +The highlight function takes 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 the highlight function matches case insensitively." + (funcall consult--regexp-compiler input type 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." + ;; Add look-ahead wrapper only if there is more than one regular expression + (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) + (consult--minibuffer-message + "Too many regexps, %S ignored. Use post-filtering!" + (string-join (seq-drop regexps 3) " ")) + (setq regexps (seq-take regexps 3))) + (consult--join-regexps-permutations regexps (and (eq type 'emacs) "\\"))))) + +(defun consult--join-regexps-permutations (regexps esc) + "Join all permutations of REGEXPS. +ESC is the escaping string for choice and groups." + (pcase regexps + ('nil "") + (`(,r) r) + (_ (mapconcat + (lambda (r) + (concat esc "(" r esc ").*" esc "(" + (consult--join-regexps-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 "\\|"))) + +;;;; Lookup functions + +(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)))) + ;; Check that marker is alive + (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)) + +;;;; Preview support + +(defun consult--preview-rename-buffer (buf &optional name) + "Rename BUF to the preview buffer name convention. +NAME defaults to `buffer-name'." + (with-current-buffer buf + (rename-buffer (concat " Preview:" (or name (buffer-name))) 'unique))) + +(defun consult--preview-add-buffer (list buf &optional name) + "Add BUF to LIST and rename BUF to the preview buffer name convention. +NAME defaults to `buffer-name'. Kill old buffers if the list length +exceeds `consult-preview-max-count'." + (consult--preview-rename-buffer (cdr buf) name) + (push buf list) + (while (length> list consult-preview-max-count) + (kill-buffer (cdar (last list))) + (setq list (nbutlast list))) + list) + +(defun consult--preview-allowed-p (fun) + "Return non-nil if FUN is an allowed preview mode hook." + (or (memq fun consult-preview-allowed-hooks) + (when-let* (((symbolp fun)) + (name (symbol-name fun)) + ;; Global modes in Emacs 29 are activated via a + ;; `find-file-hook' ending with `-check-buffers'. This has been + ;; changed in Emacs 30. Now a `change-major-mode-hook' is used + ;; instead with the suffix `-check-buffers'. + (suffix (static-if (>= emacs-major-version 30) + "-enable-in-buffer" + "-check-buffers")) + ((string-suffix-p suffix name))) + (memq (intern (string-remove-suffix suffix name)) + consult-preview-allowed-hooks)))) + +(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 #'consult--preview-allowed-p + (default-value 'find-file-hook))) + (find-file-hook (default-value 'find-file-hook))) + (apply orig hooks)) + (apply orig hooks))) + +(defun consult--minibuffer-message (&rest msg) + "Show MSG in the minibuffer without logging." + (with-selected-window (or (active-minibuffer-window) (selected-window)) + (let (message-log-max minibuffer-message-timeout) + (apply #'minibuffer-message msg)))) + +(defun consult--find-file-temporarily-1 (name) + "Open file NAME, helper function for `consult--find-file-temporarily'." + ;; file-attributes may throw permission denied error + (when-let* ((attrs (ignore-errors (file-attributes name))) + (size (file-attribute-size attrs))) + (let* ((partial (>= size consult-preview-partial-size)) + (buffer (if partial + (generate-new-buffer (format "consult-partial-preview-%s" name)) + (find-file-noselect name 'nowarn))) + (success nil)) + (unwind-protect + (with-current-buffer buffer + (if (not partial) + (when (or (eq major-mode 'hexl-mode) + (and (eq major-mode 'fundamental-mode) + (save-excursion (search-forward "\0" nil 'noerror)))) + (error "No preview of binary file")) + (with-silent-modifications + (setq buffer-read-only t) + (insert-file-contents name nil 0 consult-preview-partial-chunk) + (goto-char (point-max)) + (insert "\nFile truncated. End of partial preview.\n") + (goto-char (point-min))) + (when (save-excursion (search-forward "\0" nil 'noerror)) + (error "No partial preview of binary file")) + ;; Auto detect major mode and hope for the best, given that the + ;; file is only previewed partially. If an error is thrown the + ;; buffer will be killed and preview is aborted. + (set-auto-mode) + (font-lock-mode 1)) + (when (bound-and-true-p so-long-detected-p) + (error "No preview of file with long lines")) + ;; Run delayed hooks listed in `consult-preview-allowed-hooks'. + (dolist (hook (reverse (cons 'after-change-major-mode-hook delayed-mode-hooks))) + (run-hook-wrapped hook (lambda (fun) + (when (consult--preview-allowed-p fun) + (funcall fun)) + nil))) + (setq success (current-buffer))) + (unless success + (kill-buffer buffer)))))) + +(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)))) + (condition-case err + (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)) + (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))) + (error + (consult--minibuffer-message "%s" (error-message-string err)) + nil)))) + +(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 (_) + ;; Fully initialize previewed files and keep them alive. + (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 (consult--file-action 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 (let (file-name-handler-alist) + (abbreviate-file-name (expand-file-name name)))) + (or + ;; Find existing fully initialized buffer (non-previewed). We have + ;; to check for fully initialized buffer before accessing the + ;; previewed buffers, since `embark-act' can open a buffer which is + ;; currently previewed, such that we end up with two buffers for + ;; the same file - one previewed and only partially initialized and + ;; one fully initialized. In this case we prefer the fully + ;; initialized buffer. For directories `get-file-buffer' returns nil, + ;; therefore we have to special case Dired. + (let (file-name-handler-alist) + (if (and (fboundp 'dired-find-buffer-nocreate) (file-directory-p name)) + (dired-find-buffer-nocreate name) + (get-file-buffer name))) + ;; Find existing previewed buffer. Previewed buffers are not fully + ;; initialized (hooks are delayed) in order to ensure fast preview. + (cdr (assoc name temporary-buffers)) + ;; If no existing buffer has been found, open the file for preview. + (when-let* (((not (seq-find (lambda (x) (string-match-p x name)) + consult-preview-excluded-files))) + (buf (consult--find-file-temporarily name))) + ;; Only add new buffer if not already in the list + (unless (or (rassq buf temporary-buffers) (memq buf orig-buffers)) + (add-hook 'window-selection-change-functions hook) + (cl-callf consult--preview-add-buffer temporary-buffers + (cons name buf) (file-name-nondirectory (directory-file-name name))) + ;; Disassociate buffer from file by setting `buffer-file-name' + ;; and `dired-directory' to nil. This lets us open an already + ;; previewed buffer with the Embark default action C-. RET. + ;; The buffer disassociation is delayed to avoid breaking modes + ;; like `pdf-view-mode' or `doc-view-mode' which rely on + ;; `buffer-file-name'. Executing (set-visited-file-name nil) + ;; early also prevents the major mode initialization. + (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))) + buf))) + (remove-hook 'window-selection-change-functions hook) + (pcase-dolist (`(,_ . ,buf) temporary-buffers) + (kill-buffer buf)) + (setq temporary-buffers nil))))) + +(defun consult--invisible-open-permanently () + "Open overlays which hide the current line. +See `isearch-open-necessary-overlays' and `isearch-open-overlay-temporary'." + (dolist (ov (overlays-in (pos-bol) (pos-eol))) + (when-let* ((fun (overlay-get ov 'isearch-open-invisible)) + ((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'." + (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-ensure-buffer (pos) + "Ensure that buffer of marker POS is displayed, return t if successful." + (or (not (markerp pos)) + ;; Switch to buffer if it is not visible + (when-let* ((buf (marker-buffer pos))) + (or (and (eq (current-buffer) buf) (eq (window-buffer) buf)) + (if-let* ((win (get-buffer-window buf))) + (select-window win 'norecord) + (consult--buffer-action buf 'norecord)) + t)))) + +(defun consult--jump (pos) + "Jump to POS. +First push current position to mark ring, then move to new +position and run `consult-after-jump-hook'." + (when pos + ;; Extract marker from list with with overlay positions, see `consult--line-match' + (when (consp pos) (setq pos (car pos))) + ;; When the marker is in the same buffer, record previous location + ;; such that the user can jump back quickly. + (when (or (not (markerp pos)) (eq (current-buffer) (marker-buffer pos))) + ;; push-mark mutates markers in the mark-ring and the mark-marker. + ;; Therefore we transform the marker to a number to be safe. + ;; We all love side effects! + (setq pos (+ pos 0)) + (push-mark (point) t)) + (when (consult--jump-ensure-buffer pos) + (unless (= (goto-char pos) (point)) ;; Widen if jump failed + (widen) + (goto-char 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 (restore) + (lambda (action cand) + (when (eq action 'preview) + (mapc #'funcall restore) + (setq restore nil) + ;; TODO Better buffer preview support + ;; 1. Use consult--buffer-preview instead of consult--jump-ensure-buffer + ;; 2. Remove function consult--jump-ensure-buffer + ;; 3. Remove consult-buffer-other-* from consult-customize-alist + (when-let* ((pos (or (car-safe cand) cand)) ;; Candidate can be previewed + ((consult--jump-ensure-buffer pos))) + (let ((saved-min (point-min-marker)) + (saved-max (point-max-marker)) + (saved-pos (point-marker))) + (set-marker-insertion-type saved-max t) ;; Grow when text is inserted + (push (lambda () + (when-let* ((buf (marker-buffer saved-pos))) + (with-current-buffer buf + (narrow-to-region saved-min saved-max) + (goto-char saved-pos) + (set-marker saved-pos nil) + (set-marker saved-min nil) + (set-marker saved-max nil)))) + restore)) + (unless (= (goto-char pos) (point)) ;; Widen if jump failed + (widen) + (goto-char pos)) + (setq restore (nconc (consult--invisible-open-temporarily) restore)) + ;; Ensure that cursor is properly previewed (gh:minad/consult#764) + (unless (eq cursor-in-non-selected-windows 'box) + (let ((orig cursor-in-non-selected-windows) + (buf (current-buffer))) + (push + (if (local-variable-p 'cursor-in-non-selected-windows) + (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (setq-local cursor-in-non-selected-windows orig)))) + (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (kill-local-variable 'cursor-in-non-selected-windows))))) + restore) + (setq-local cursor-in-non-selected-windows 'box))) + ;; Match previews + (let ((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) + 'category 'consult-preview-line-overlay + 'window (selected-window))))))) + (dolist (match (cdr-safe cand)) + (push (consult--make-overlay (+ (point) (car match)) + (+ (point) (cdr match)) + 'category 'consult-preview-match-overlay + 'window (selected-window)) + overlays)) + (push (lambda () (mapc #'delete-overlay overlays)) restore)) + (run-hooks 'consult-after-jump-hook)))))) + +(put 'consult-preview-line-overlay 'face 'consult-preview-line) +(put 'consult-preview-line-overlay 'priority 1) +(put 'consult-preview-match-overlay 'face 'consult-preview-match) +(put 'consult-preview-match-overlay 'priority 2) + +(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)) + ;; Transform cheap marker to real marker + (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) + (setq key (consult--key-parse 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." + ;; Symbol indirection because of bug#46407. + (let ((hook (make-symbol "consult--preview-post-command-hook"))) + (fset hook fun) + ;; TODO Emacs 28 has a bug, where the hook--depth-alist is not cleaned up properly + ;; Do not use the broken add-hook here. + ;;(add-hook 'post-command-hook hook 'append 'local) + (setq-local post-command-hook + (append + (remove t post-command-hook) + (list hook) + (and (memq t post-command-hook) '(t)))))) + +(defun consult--with-preview-f (preview-key state transform candidate save-input body) + "See `consult--with-preview' for documentation." + (let ((mb-input "") (timer (timer-create)) mb-narrow selected previewed) + (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) + (cancel-timer timer) + (with-selected-window (consult--original-window) + ;; STEP 3: Reset preview + (when previewed + (funcall state 'preview nil)) + ;; STEP 4: Notify the preview function of the minibuffer exit + (funcall state 'exit nil))))) + (add-hook 'minibuffer-exit-hook hook)) + ;; STEP 1: Setup the preview function + (with-selected-window (consult--original-window) + (funcall state 'setup nil)) + (setq consult--preview-function + (lambda () + (when-let* ((cand (funcall candidate))) + ;; Drop properties to prevent bugs regarding candidate + ;; lookup, which must handle candidates without + ;; properties. Otherwise the arguments passed to the + ;; lookup function are confusing, since during preview + ;; the candidate has properties but for the final lookup + ;; after completion it does not. + (setq cand (substring-no-properties cand)) + (with-selected-window (active-minibuffer-window) + (let ((input (minibuffer-contents-no-properties)) + (narrow consult--narrow) + (win (consult--original-window))) + (with-selected-window win + (when-let* ((transformed (funcall transform narrow input cand)) + (debounce (consult--preview-key-debounce preview-key transformed))) + (cancel-timer timer) + ;; The transformed candidate may have text + ;; properties, which change the preview display. + ;; This matters for example for `consult-grep', + ;; where the current candidate and input may + ;; stay equal, but the highlighting of the + ;; candidate changes while the candidates list + ;; is lagging a bit behind and updates + ;; asynchronously. + ;; + ;; In older Consult versions we instead compared + ;; the input without properties, since I worried + ;; that comparing the transformed candidates + ;; could be potentially expensive. However + ;; comparing the transformed candidates is more + ;; correct. The transformed candidate is the + ;; thing which is actually previewed. + (unless (equal-including-properties previewed transformed) + (if (> debounce 0) + (progn + (timer-set-function + timer + (lambda () + ;; Preview only when a completion + ;; window is selected and when + ;; the preview window is alive. + (when (and (consult--completion-window-p) + (window-live-p win)) + (with-selected-window win + ;; STEP 2: Preview candidate + (funcall state 'preview (setq previewed transformed)))))) + (timer-set-time timer (timer-relative-time nil debounce)) + (timer-activate timer)) + ;; STEP 2: Preview candidate + (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 + (setq selected (when-let* ((result (funcall body))) + (when-let* ((save-input) + (list (symbol-value save-input)) + ((equal (car list) result))) + (set save-input (cdr list))) + (funcall transform mb-narrow mb-input result))) + (when save-input + (add-to-history save-input mb-input)) + (when state + ;; STEP 5: The preview function should perform its final action + (funcall state 'return selected)))))) + +(defmacro consult--with-preview (preview-key state transform candidate save-input &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. +SAVE-INPUT can be a history variable symbol to save the input. + +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 `consult--original-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 5) (debug t)) + `(consult--with-preview-f ,preview-key ,state ,transform ,candidate ,save-input (lambda () ,@body))) + +;;;; Narrowing and grouping + +(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 + (consult--key-parse consult-widen-key)) + (consult-narrow-key + (let ((key (consult--key-parse consult-narrow-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'." + (declare (completion ignore)) + (interactive + (list (unless (equal (this-single-command-keys) (consult--widen-key)) + last-command-event))) + (consult--require-minibuffer) + (setq consult--narrow key) + (when-let* ((pred (plist-get consult--narrow-config :predicate))) + (setq minibuffer-completion-predicate (and consult--narrow pred))) + (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 + (format #(" [%s]" 0 5 (face consult-narrow-indicator)) + (alist-get consult--narrow + (plist-get consult--narrow-config :keys)))))) + (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* ((keys (plist-get consult--narrow-config :keys)) + (pair (or (and (length= str 1) (assoc (aref str 0) keys)) + (and (equal str "") (assoc ?\s 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." + (declare (completion ignore)) + (interactive) + (consult--require-minibuffer) + (consult--minibuffer-message + (mapconcat (lambda (x) + (concat + (propertize (key-description (list (car x))) 'face 'consult-key) + " " + (propertize (cdr x) 'face 'consult-help))) + (plist-get consult--narrow-config :keys) + " "))) + +(defun consult--narrow-setup (config map) + "Setup narrowing with CONFIG and keymap MAP." + (setq consult--narrow-config (if (memq :keys config) + config (list :keys config))) + (when-let* ((key consult-narrow-key)) + (setq key (consult--key-parse key)) + (dolist (pair (plist-get consult--narrow-config :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)))) + +;;;; Splitting completion style + +(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) + ;; Force update if two punctuation characters are entered. + `(,(propertize (match-string 1 str) 'consult--force (match-end 2)) + ,(match-end 0) + ;; List of highlights + (0 . ,(match-beginning 1)) + ,@(and (match-end 2) `((,(match-beginning 2) . ,(match-end 2))))))) + `(,str ,(length str)))) + +(defun consult--split-none (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) + ;; Force update if separator is entered. + `(,(propertize (match-string 1 str) 'consult--force (match-end 2)) + ,(match-end 0) + ;; List of highlights + ,@(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." + (when (equal completion-styles '(consult--split)) + (error "`consult--async-split-input' initialized twice")) + (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) + (setq newstr (concat (substring str 0 pos) newstr)) + (if (eq (cadr (funcall split newstr)) pos) + (cons newstr (+ pos newpt)) + (cons str point))))))) + (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))) + +;;;; Asynchronous pipeline + +(defun consult--async-pipeline (&rest async) + "Compose ASYNC pipeline. + +An async function must accept a single SINK argument and return a +function accepting a single ACTION argument. In functional programming +terminology, an async function is curried. + + (lambda (sink) + (lambda (action) + ...)) + +Async functions are composed with `consult--async-pipeline' as in the +following example. The data flows downwards starting with the input +from the user. + + (consult--async-pipeline + (consult--async-min-input) + (consult--async-throttle) + (consult--async-process #\\='consult--man-builder) + (consult--async-transform #\\='consult--man-format) + (consult--async-highlight #\\='consult--man-builder)) + +Nil functions are ignored to ease building conditional pipelines. + + (consult--async-pipeline + (consult--async-min-input min-input) + (consult--async-throttle throttle debounce) + (consult--async-dynamic fun) + transform + (and highlight (consult--async-highlight highlight))) + +Async functions or pipelines can be passed as completion function to +`consult--read' or used as `:async' field of `consult--multi' sources as +shown in these examples: + + (consult--read (consult--async-pipeline ...)) + (consult--read (consult--dynamic-collection (lambda (input) ...))) + (consult--read (consult--process-collection #\\='consult--man-builder)) + + (defvar async-source + (list :async (consult--async-pipeline ...))) + (defvar dynamic-source + (list :async (consult--dynamic-collection (lambda (input) ...)))) + (defvar command-source + (list :async (consult--process-collection #\\='consult--man-builder))) + +Incoming candidates and the action argument should be passed to the +sink. The action can take the following forms: + +\\='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. +\\='cancel Cancel any running process. Return nil. +nil Return the list of candidates. +list Append to the existing candidates list and return the whole list. +string Update with the current user input string. Return nil. + +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." + (lambda (sink) + (seq-reduce (lambda (s f) (funcall f s)) (delq nil (reverse async)) sink))) + +(defun consult--async-wrap (async) + "Wrap ASYNC function with the default pipeline. +The default pipeline provides `consult--async-split', +`consult--async-indicator' and `consult--async-refresh'." + (consult--async-pipeline + (consult--async-split) + async + (consult--async-indicator) + (consult--async-refresh))) + +(defun consult--async-p (fun) + "Return t if FUN is an asynchronous function." + (and (functionp fun) (equal (func-arity fun) '(1 . 1)))) + +(defmacro consult--with-async (async &rest body) + "Setup asynchronous completion in BODY. +ASYNC is the asynchronous function or completion table." + (declare (indent 1) (debug (symbolp body))) + `(consult--with-async-f ,async (lambda (,async) ,@body))) + +(defun consult--with-async-f (async body) + "See `consult--with-async' for documentation." + (let (new-chunk orig-chunk) + (minibuffer-with-setup-hook + ;; Append such that we overwrite the completion style setting of + ;; `fido-mode'. See `consult--async-split' and `consult--split-setup'. + (:append + (lambda () + (when (consult--async-p async) + (setq new-chunk (max read-process-output-max consult--process-chunk) + 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)) + ;; Push input string to request refresh. + (funcall async (minibuffer-contents-no-properties)))))))) + ;; We use a symbol in order to avoid adding lambdas to + ;; the hook variable. Symbol indirection because of + ;; bug#46407. + (hook (make-symbol "consult--async-after-change-hook")) + (timer (timer-create))) + (timer-set-function timer fun) + ;; Delay modification hook to ensure that minibuffer is still + ;; alive after the change, such that we don't restart a new + ;; asynchronous search right before exiting the minibuffer. + (fset hook (lambda (&rest _) + (unless (memq timer timer-list) + (timer-set-time timer (current-time)) + (timer-activate timer)))) + (add-hook 'after-change-functions hook nil 'local) + ;; Immediately start asynchronous computation. This may lead + ;; to problems unnecessary work if content is inserted shortly + ;; afterwards. + (funcall fun))))) + (let ((async (if (consult--async-p async) async (lambda (_) async)))) + (unwind-protect + (funcall body async) + (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 () + "Asynchronous sink function." + (let (candidates last buffer) + (lambda (action) + (pcase-exhaustive action + ('setup + (setq buffer (current-buffer)) + nil) + ((or (pred stringp) 'destroy 'cancel) nil) + ('flush (setq candidates nil last nil)) + ('refresh + ;; Refresh the UI when the current minibuffer window belongs + ;; to the current asynchronous completion session. + (when-let* ((win (active-minibuffer-window))) + (when (eq (window-buffer win) buffer) + (with-selected-window win + (run-hooks 'consult--completion-refresh-hook) + ;; Interaction between asynchronous completion functions and + ;; preview: We have to trigger preview immediately when + ;; candidates arrive (gh:minad/consult#436). + (when (and consult--preview-function candidates) + (funcall consult--preview-function))))) + nil) + ('nil candidates) + ((pred consp) + ;; Lazily initialize last link, such that it is only initialized when + ;; appending, and not for one-shot async functions like + ;; `consult--async-static'. + (if (not candidates) + (setq candidates action) + (setq last (last (setcdr (or last (last candidates)) action))) + candidates)))))) + +(defun consult--async-dynamic (fun &optional restart) + "Dynamic computation of candidates. +FUN computes the candidates. It takes either a single input argument or +an input argument and a callback function, if computed candidates should +be updated incrementally. The callback function must not be called +after FUN has returned. +RESTART is the time after which an interrupted computation should be +restarted and defaults to `consult-async-input-debounce'." + (setq restart (or restart consult-async-input-debounce)) + (when (equal (func-arity fun) '(1 . 1)) + (let ((orig fun)) + (setq fun (lambda (input callback) + (funcall callback (funcall orig input)))))) + (lambda (sink) + (let ((timer (timer-create)) (current nil) (compute nil)) + (setq compute + (lambda (input) + (cancel-timer timer) + (funcall sink [indicator running]) + (redisplay) + (let* ((state 'init) + (killed + (while-no-input + (funcall + fun input + (lambda (response) + (when (eq state 'done) + (error "consult--async-dynamic: Callback called too late")) + (let (throw-on-input) + (when (eq state 'init) + (funcall sink 'flush) + (setq state 'running)) + (when response + (funcall sink response) + ;; Accept process input such that timers + ;; trigger and refresh the completion UI. + (accept-process-output))))) + (setq current input + state 'done) + nil))) + (funcall sink `[indicator ,(if killed 'killed 'finished)]) + (funcall sink 'refresh) + ;; If the computation was killed, restart it after a while. + ;; This happens when the point is moved. Then the input does + ;; not change and the computation is not restarted otherwise. + (when (and killed (not (memq timer timer-list))) + (timer-set-function timer compute (list input)) + (timer-set-time timer (timer-relative-time nil restart)) + (timer-activate timer))))) + (lambda (action) + (prog1 (funcall sink action) + (pcase action + ((or 'cancel 'destroy) (cancel-timer timer)) + ((pred stringp) + (if (not (equal action current)) + (funcall compute action) + (cancel-timer timer) + (funcall sink [indicator finished]))))))))) + +(defun consult--async-static (items) + "Async function with static ITEMS." + (consult--async-dynamic + (lambda (input) + (pcase-let ((`(,re . ,hl) (consult--compile-regexp + input 'emacs completion-ignore-case))) + (if re + (let* ((completion-regexp-list re) + (all (all-completions "" items))) + (cl-loop for s in-ref all do + (funcall hl (setf s (copy-sequence s)))) + all) + (copy-sequence items)))))) + +(defun consult--async-merge-sink (sink indicator tail idx) + "Create sink for the async sub-functions which merges the sub-lists. +SINK is the joined sink. +INDICATOR is a vector of indicator symbols. +TAIL is a vector of list tail links for each sub-list. +IDX is the index of the corresponding link in TAIL." + (lambda (action) + (pcase action + (`[indicator ,state] + (aset indicator (1- idx) state) + (let* ((severity [nil finished running killed failed]) + (state (aref severity (cl-loop for i across indicator maximize + (or (seq-position severity i) 0))))) + (funcall sink `[indicator ,state]))) + ('flush + ;; Flush items if sub-list exists. + (when-let* ((tl (aref tail idx)) (pre t)) + (let ((i idx)) (while (not (setq pre (aref tail (decf i)))))) + (setcdr pre (cdr tl)) + (aset tail idx nil) + (funcall sink 'flush) + (funcall sink (cdr (aref tail 0))))) + ((pred consp) + (let ((tl (aref tail idx)) + (last (last action)) + pre) + (aset tail idx last) + (if tl ;; Append items if sub-list exists. + (progn + (setcdr last (cdr tl)) + (setcdr tl action)) + ;; Otherwise insert new sub-list. + (let ((i idx)) (while (not (setq pre (aref tail (decf i)))))) + (setcdr last (cdr pre)) + (setcdr pre action)) + (funcall sink 'flush) + (funcall sink (cdr (aref tail 0)))))))) + +(defun consult--async-merge (asyncs) + "Create merged async function from multiple ASYNCS." + (lambda (sink) + (let* ((indicator (make-vector (length asyncs) nil)) + (tail (make-vector (1+ (length indicator)) nil)) + (asyncs + (seq-map-indexed + (lambda (fun idx) + (funcall fun (consult--async-merge-sink sink indicator tail (1+ idx)))) + asyncs))) + (aset tail 0 (list nil)) ;; Guard element + (lambda (action) + (dolist (async asyncs) + (funcall async action)) + (funcall sink action))))) + +(defun consult--async-debug (prefix) + "Async function with debug messages. +The messages are prefixed with PREFIX." + (lambda (sink) + (lambda (action) + (consult--async-log "%s: %S\n" prefix action) + (funcall sink action)))) + +(defun consult--async-predicate (pred) + "Async function running only if PRED is non-nil." + (lambda (sink) + (let (input) + (lambda (action) + (prog1 (and (not (stringp action)) + (funcall sink action)) + (pcase action + ('setup (setq pred (consult--in-buffer pred))) + ((or 'cancel 'destroy) (setq input nil)) + ((pred stringp) (setq input action))) + (when (and input (funcall pred)) + (funcall sink input) + (setq input nil))))))) + +(defun consult--async-min-input (&optional min-input) + "Async function enforcing a minimum input length. +MIN-INPUT is the minimum input length and defaults to +`consult-async-min-input'." + (setq min-input (or min-input consult-async-min-input)) + (lambda (sink) + (lambda (action) + (if (stringp action) + ;; Input can be marked with the `consult--force' property such that it + ;; is passed through in any case. + (funcall sink (if (or (and (not (equal action "")) + (get-text-property 0 'consult--force action)) + (>= (length action) min-input)) + action 'cancel)) + (funcall sink action))))) + +(defun consult--async-split (&optional style) + "Async function, which splits the input string. +STYLE is the splitting style and defaults to the splitting style +configured by `consult-async-split-style'." + (setq style (or style consult-async-split-style 'none) + style (or (alist-get style consult-async-split-styles-alist) + (user-error "Splitting style `%s' not found" style))) + (lambda (sink) + (lambda (action) + (pcase action + ('setup + (consult--split-setup (let ((fun (plist-get style :function))) + (lambda (str) (funcall fun str style)))) + (when-let* ((initial (plist-get style :initial))) + (save-excursion + (goto-char (minibuffer-prompt-end)) + (unless (equal initial (char-after)) + (insert-before-markers initial)))) + (funcall sink 'setup)) + ((pred stringp) + (pcase-let ((`(,input ,_ . ,highlights) + (funcall (plist-get style :function) action style)) + (end (minibuffer-prompt-end))) + ;; Highlight punctuation characters + (pcase-dolist (`(,x . ,y) highlights) + (add-text-properties (+ end x) (+ end y) + '(face consult-async-split consult--split t rear-nonsticky t))) + (funcall sink input))) + (_ (funcall sink action)))))) + +(defun consult--async-options () + "Async function, which highlights commands options in the input string." + (lambda (sink) + (lambda (action) + (when (stringp action) + (save-match-data + (when-let* ((iend (save-excursion + (goto-char (minibuffer-prompt-end)) + (search-forward action nil t))) + (ibeg (- iend (length action)))) + (remove-list-of-text-properties ibeg iend '(face rear-nonsticky)) + (when-let* (((string-match "\\(?:\\`\\| \\)\\(-\\)" action)) + (beg (match-beginning 1)) + ((string-match "\\(?:\\`\\| \\)\\(--\\)\\(?: \\|\\'\\)\\|\\'" action)) + (end (or (match-end 1) (match-end 0)))) + (add-text-properties (+ ibeg beg) (+ ibeg end) + '( face consult-async-option + rear-nonsticky t)))))) + (funcall sink action)))) + +(defun consult--async-indicator () + "Async function with a state indicator overlay." + (lambda (sink) + (let ((ind (cl-loop for (k c f) in consult-async-indicator + collect (cons k (propertize (string c) 'face f)))) + ov) + (lambda (action) + (pcase action + ('setup + (dolist (ov (overlays-at (- (minibuffer-prompt-end) 2))) + (when (eq (overlay-get ov 'category) 'consult-async-indicator-overlay) + (error "`consult--async-indicator' initialized twice"))) + (setq ov (consult--make-overlay + (- (minibuffer-prompt-end) 2) + (- (minibuffer-prompt-end) 1) + 'category 'consult-async-indicator-overlay)) + (funcall sink 'setup)) + ('destroy + (delete-overlay ov) + (funcall sink 'destroy)) + (`[indicator ,state] + (overlay-put ov 'display (alist-get state ind))) + (_ (funcall sink 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 (builder &rest props) + "Async process function. +BUILDER is the command line builder function. +PROPS are optional properties passed to `make-process'." + (lambda (sink) + (let (proc proc-buf last-args count) + (lambda (action) + (pcase action + ((pred stringp) + (funcall sink action) + (let ((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 + (let* ((flush t) + (rest "") + (proc-filter + (lambda (_ out) + (when flush + (setq flush nil) + (funcall sink '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 sink lines)))))) + (proc-sentinel + (lambda (_ event) + (cond + (flush + (setq flush nil) + (funcall sink 'flush)) + ((and (string-prefix-p "finished" event) (not (equal rest ""))) + (incf count) + (funcall sink (list rest)))) + (funcall sink `[indicator + ,(cond + ((string-prefix-p "killed" event) 'killed) + ((string-prefix-p "finished" event) 'finished) + (t 'failed))]) + (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"))))) + (process-adaptive-read-buffering nil)) + (funcall sink [indicator running]) + (consult--async-log "consult--async-process started: args=%S default-directory=%S\n" + args default-directory) + (setq count 0 + proc-buf (generate-new-buffer " *consult-async-stderr*") + proc (apply #'make-process + `(,@props + :connection-type pipe + :name ,(car args) + ;;; XXX tramp bug, the stderr buffer must be empty + :stderr ,proc-buf + :noquery t + :command ,args + :filter ,proc-filter + :sentinel ,proc-sentinel))))))) + nil) + ((or 'cancel 'destroy) + (when proc + (delete-process proc) + (kill-buffer proc-buf) + (setq proc nil proc-buf nil)) + (setq last-args nil) + (funcall sink action)) + (_ (funcall sink action))))))) + +(defun consult--async-highlight (&optional highlight) + "Async function with candidate highlighting. +HIGHLIGHT is a function called with the input string. It should return +a function which mutably adds highlighting to a candidate string. +HIGHLIGHT can also return a pair where the second element is the actual +highlight function. If not given, HIGHLIGHT defaults to a function +which highlights words." + (unless (functionp highlight) + (setq highlight + (lambda (input) + (consult--compile-regexp input 'emacs completion-ignore-case)))) + (consult--async-transform-by-input + (lambda (input) + (when-let* ((hl (funcall highlight input)) + (hl (if (functionp hl) hl (cdr hl)))) + (lambda (cands) + (dolist (x cands cands) + (funcall hl (if (consp x) (car x) x)))))))) + +(defun consult--async-throttle (&optional throttle debounce) + "Async function 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)) + (lambda (sink) + (let ((timer (timer-create)) (last 0) initial-p input) + (lambda (action) + (pcase action + ((pred stringp) + (unless (equal action input) + (cancel-timer timer) + (funcall sink 'cancel) + (timer-set-function timer (lambda () + (setq last (float-time)) + (funcall sink action))) + (timer-set-time + timer + (timer-relative-time + ;; Debounce only if the user entered new input. Start + ;; immediately if the minibuffer contains initial input. + nil (max (if (funcall initial-p) 0 debounce) + (- (+ last throttle) (float-time))))) + (setq input action) + (timer-activate timer)) + nil) + ('setup + (setq initial-p + (consult--in-buffer + (let ((initial (minibuffer-contents-no-properties))) + (lambda () + (equal initial (minibuffer-contents-no-properties)))))) + (funcall sink action)) + ((or 'cancel 'destroy) + (cancel-timer timer) + (funcall sink action)) + (_ (funcall sink action))))))) + +(defun consult--async-refresh (&optional delay) + "Async function which refreshes the display with a timer. +The refresh happens after a DELAY, defaulting to +`consult-async-refresh-delay'." + (setq delay (or delay consult-async-refresh-delay)) + (lambda (sink) + (if (<= delay 0) + (lambda (action) + (pcase action + ((or (pred consp) 'flush) + (prog1 (funcall sink action) + (funcall sink 'refresh))) + (_ (funcall sink action)))) + (let ((timer (timer-create))) + (lambda (action) + (prog1 (funcall sink action) + (pcase action + ((or (pred consp) 'flush) + (unless (memq timer timer-list) + (timer-set-function timer sink '(refresh)) + (timer-set-time timer (timer-relative-time nil delay)) + (timer-activate timer))) + ((or 'destroy 'refresh) ;; 'refresh already forced a refresh + (cancel-timer timer))))))))) + +(defun consult--async-transform-by-input (fun) + "Transform candidates via FUN. +FUN takes the input string and must return a transformation function." + (lambda (sink) + (let (transform) + (lambda (action) + (cond + ((stringp action) + (setq transform (funcall fun action)) + (funcall sink action)) + ((and (consp action) transform) + (funcall sink (funcall transform action))) + (t (funcall sink action))))))) + +(defun consult--async-transform (fun) + "Use FUN to transform candidates." + (lambda (sink) + (lambda (action) + (funcall sink (if (consp action) (funcall fun action) action))))) + +(defun consult--async-map (fun) + "Map candidates by FUN." + (consult--async-transform (apply-partially #'mapcar fun))) + +(defun consult--async-filter (fun) + "Filter candidates by FUN." + (consult--async-transform (apply-partially #'seq-filter fun))) + +;;;; Prebuilt async pipelines + +(cl-defun consult--dynamic-collection (fun &key min-input throttle debounce + transform highlight) + "Dynamic candidate computation pipeline. +FUN computes the candidates. It takes either a single input argument or +an input argument and a callback function, if computed candidates should +be updated incrementally. The callback function must not be called +after FUN has returned. +MIN-INPUT is passed to `consult--async-min-input'. +THROTTLE and DEBOUNCE are passed to `consult--async-throttle'. +TRANSFORM is an optional async function transforming the candidate. +HIGHLIGHT is an optional highlight function, can be t for the default +highlighting function." + (declare (indent 1)) + (consult--async-pipeline + (consult--async-min-input min-input) + (consult--async-throttle throttle debounce) + (consult--async-dynamic fun) + transform + (and highlight (consult--async-highlight highlight)))) + +(cl-defun consult--process-collection (builder &rest props &key min-input + debounce throttle transform + highlight &allow-other-keys) + "Asynchronous process pipeline. +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. +TRANSFORM is an optional async function transforming the candidate. +If HIGHLIGHT is non-nil, highlight the candidates. +MIN-INPUT is passed to `consult--async-min-input'. +THROTTLE and DEBOUNCE are passed to `consult--async-throttle'. +Other PROPS are passed to `make-process'." + (declare (indent 1)) + (consult--async-pipeline + (consult--async-options) + (consult--async-min-input min-input) + (consult--async-throttle throttle debounce) + (apply #'consult--async-process builder + (consult--plist-remove + '(:min-input :throttle :debounce :transform :highlight) props)) + transform + (and highlight (consult--async-highlight + (if (functionp highlight) highlight builder))))) + +;;;; Special keymaps + +(defvar-keymap consult-async-map + :doc "Keymap added for commands with asynchronous candidates." + ;; Overwriting some unusable defaults of default minibuffer completion. + "<remap> <minibuffer-complete-word>" #'self-insert-command + ;; Remap Emacs 29 history and default completion for now + ;; (gh:minad/consult#613). + "<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) + +;;;; Internal API: consult--read + +(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." + (setq items + (delete-dups + (append + ;; Defaults are at the beginning of the future history + (ensure-list minibuffer-default) + ;; Custom items + (remove "" (remq nil (ensure-list items))) + ;; Add all completions for non-async commands. For async commands + ;; this feature is not useful, since if one selects a completion + ;; candidate, the async search is restarted using that candidate + ;; string. This usually does not yield a desired result since the + ;; async input uses a special format, e.g., `#grep#filter'. + (unless async + (all-completions "" minibuffer-completion-table + minibuffer-completion-predicate))))) + ;; Prefix all items with the initial input from the async split style. + (when (and async (get-text-property (minibuffer-prompt-end) 'consult--split)) + (let* ((beg (minibuffer-prompt-end)) + (end (or (text-property-any beg (point-max) 'consult--split nil) + (point-max))) + (pre (buffer-substring beg end))) + (cl-loop for item in-ref items do + (unless (string-prefix-p pre item) + (setf item (concat pre item)))))) + items) + +(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 is the narrowing configuration. +PREVIEW-KEY are the preview keys." + (let ((old-map (current-local-map)) + (map (make-sparse-keymap))) + + ;; Add narrow keys + (when narrow + (consult--narrow-setup narrow map)) + + ;; Preview trigger keys + (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)))) + + ;; Put the keymap together + (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))) + (decf pos)) + (when (< pos max) + (add-text-properties pos max '(invisible t rear-nonsticky t cursor-intangible t))))) + +(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 "" + ;; The default completion UI adds the + ;; `completions-annotations' face if no other faces are + ;; present. + (if (text-property-not-all 0 (length ann) 'face nil ann) + ann + (propertize ann 'face 'completions-annotations)))))) + cands)) + +(cl-defun consult--read-1 ( table &key + prompt predicate require-match history default keymap category + initial narrow initial-narrow add-history annotate state + preview-key sort lookup group inherit-input-method async-wrap) + "See `consult--read' for documentation." + (when (and async-wrap (consult--async-p table)) + (setq table (funcall (funcall async-wrap table) (consult--async-sink)))) + (minibuffer-with-setup-hook + (:append (lambda () + (add-hook 'after-change-functions #'consult--tofu-hide-in-minibuffer nil 'local) + (consult--setup-keymap keymap (consult--async-p table) narrow preview-key) + (when initial-narrow (consult-narrow initial-narrow)) + (setq-local minibuffer-default-add-function + (apply-partially #'consult--add-history (consult--async-p table) add-history) + kill-transform-function #'consult--tofu-strip))) + (consult--with-async table + (consult--with-preview + preview-key state + (lambda (narrow input cand) + (funcall lookup cand (funcall table nil) input narrow)) + (apply-partially #'run-hook-with-args-until-success + 'consult--completion-candidate-hook) + (pcase-exhaustive history + (`(:input ,var) var) + ((pred symbolp))) + ;; Do not unnecessarily let-bind the lambdas to avoid over-capturing in + ;; the interpreter. This will make closures and the lambda string + ;; representation larger, which makes debugging much worse. Fortunately + ;; the over-capturing problem does not affect the bytecode interpreter + ;; which does a proper scope analysis. + (let* ((metadata `(metadata + ,@(when category `((category . ,category))) + ,@(when group `((group-function . ,group))) + ,@(when annotate + `((affixation-function + . ,(apply-partially #'consult--read-affixate annotate)))) + ,@(unless sort '((cycle-sort-function . identity) + (display-sort-function . identity))))) + (consult--annotate-align-width 0) + (selected + (completing-read + prompt + (lambda (str pred action) + (let ((result (complete-with-action action (funcall table nil) str pred))) + (if (eq action 'metadata) + (if (and (eq (car result) 'metadata) (cdr result)) + ;; Merge metadata + `(metadata ,@(cdr metadata) ,@(cdr result)) + metadata) + result))) + predicate require-match initial + (if (symbolp history) history (cadr history)) + default + inherit-input-method))) + ;; Repair the null completion semantics. `completing-read' may return + ;; an empty string even if REQUIRE-MATCH is non-nil. One can always + ;; opt-in to null completion by passing the empty string for DEFAULT. + (when (and (eq require-match t) (not default) (equal selected "")) + (user-error "No selection")) + selected))))) + +(cl-defun consult--read ( table &rest options &key + prompt predicate require-match history default command + keymap category initial narrow initial-narrow annotate + add-history state preview-key sort lookup group + inherit-input-method async-wrap) + "Enhanced completing read function to select from TABLE. + +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. You should use `completing-read' instead of +`consult--read' if you don't use asynchronous candidate +computation or candidate preview. + +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. +COMMAND is used for customization, defaulting to `this-command.' +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. +INITIAL-NARROW is an initial narrow key. +KEYMAP is a command-specific keymap. +INHERIT-INPUT-METHOD, if non-nil the minibuffer inherits the +input method. +ASYNC-WRAP wraps asynchronous functions and defaults to +`consult--async-wrap'." + (ignore prompt predicate require-match history default keymap category + initial narrow initial-narrow add-history annotate state command + preview-key sort lookup group inherit-input-method async-wrap) + (apply #'consult--read-1 table + (consult--customize-args + options + :prompt "Select: " + :preview-key consult-preview-key + :sort t + :async-wrap #'consult--async-wrap + :lookup (lambda (selected &rest _) selected)))) + +;;;; Internal API: consult--prompt + +(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." + (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)))) + (consult--with-preview + preview-key state + (lambda (_narrow inp _cand) (funcall transform inp)) + (lambda () "") + history + (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 command) + "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. +COMMAND is used for customization, defaulting to `this-command.'" + (ignore prompt history add-history initial default command + keymap state preview-key transform inherit-input-method) + (apply #'consult--prompt-1 + (consult--customize-args + options + :prompt "Input: " + :preview-key consult-preview-key + :transform #'identity))) + +;;;; Internal API: consult--multi + +(defsubst consult--multi-source (sources cand) + "Lookup source for CAND in SOURCES list." + (aref sources (consult--tofu-get cand))) + +(defsubst consult--multi-visible-p (src) + "Is SRC visible according to `consult--narrow'?" + (if-let* ((n consult--narrow)) + (pcase (plist-get src :narrow) + ((and ks `((,_ . ,_) . ,_)) (assq n ks)) + ((or `(,k . ,_) k) (eq n k))) + (not (plist-get src :hidden)))) + +(defun consult--multi-predicate (sources cand) + "Predicate function called for each candidate CAND given SOURCES." + (consult--multi-visible-p (consult--multi-source sources cand))) + +(defun consult--multi-narrow (sources) + "Return narrow list from SOURCES." + (thread-last + sources + (mapcan (lambda (src) + (when-let* ((narrow (plist-get src :narrow))) + (if (consp narrow) + (if (consp (car narrow)) (append narrow nil) (list narrow)) + (when-let* ((name (plist-get src :name))) + (list (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-filter (lambda (k) (or (eq k 'any) (stringp k))) + (seq-mapcat (lambda (src) + (ensure-list + (if (plist-member src :preview-key) + (plist-get src :preview-key) + consult-preview-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)))))) + ;; Non-existing candidate without Tofu or default submitted (empty string) + (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)) + ((seq-find (lambda (src) (not (plist-get src :hidden))) sources)) + ((aref sources 0)))) + (idx (seq-position sources src)) + (def (and (string-blank-p selected) ;; default candidate + (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))) + ;; Existing candidate submitted + (cons (cdr (get-text-property 0 'multi-category (car found))) + (consult--multi-source sources selected)) + ;; Non-existing Tofu'ed candidate submitted, e.g., via Embark + `(,(substring selected 0 -1) :match nil ,@(consult--multi-source sources selected))))) + +(defun consult--multi-items (idx src items) + "Create completion candidate strings from ITEMS. +Attach source IDX and SRC properties to each item." + (unless (listp items) + (setq items (plist-get src :items) + items (if (functionp items) (funcall items) items))) + (let ((face (plist-get src :face)) + (cat (or (plist-get src :category) 'general))) + (cl-loop + for item in items collect + (let* ((str (or (car-safe item) item)) + (len (length str)) + (cand (consult--tofu-append str idx))) + ;; Preserve existing `multi-category' datum of the candidate. + (unless (and (eq str item) (get-text-property 0 'multi-category str)) + (put-text-property 0 len 'multi-category (cons cat (or (cdr-safe item) item)) cand)) + (when face + (add-face-text-property 0 len face t cand)) + cand)))) + +(defun consult--multi-async (sources) + "Create async function from multi SOURCES." + (consult--async-merge + (cl-loop + for idx from 0 for src across sources collect + (let ((idx idx) (src src)) + (consult--async-pipeline + (consult--async-predicate (apply-partially #'consult--multi-visible-p src)) + (if-let* ((async (plist-get src :async))) + (consult--async-pipeline + async + (consult--async-transform + (apply-partially #'consult--multi-items idx src))) + (consult--async-static (consult--multi-items idx src t)))))))) + +(defun consult--multi-enabled-sources (sources) + "Return vector of enabled SOURCES." + (vconcat + (cl-loop + for src in sources + if (when (setq src (if (symbolp src) (symbol-value src) src)) + (unless (xor (plist-member src :async) (plist-member src :items)) + (error "Source must specify either :items or :async")) + (funcall (or (plist-get src :enabled) #'always))) + collect src))) + +(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)))) + ;; If the candidate source changed during preview communicate to + ;; the last source, that none of its candidates is previewed anymore. + (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)))) + ;; Finish all the sources, except the selected one. + (pcase-dolist (`(,_ . ,fun) states) + (unless (eq fun selected-fun) + (funcall fun 'return nil))) + ;; Finish the source with the selected candidate + (when selected-fun + (funcall selected-fun 'return cand))))))))) + +(defun consult--multi-collection (sources) + "Static or asynchronous completion function from SOURCES." + (consult--with-increased-gc + (if (cl-loop for src across sources thereis (plist-get src :async)) + (consult--multi-async sources) + (cl-loop for idx from 0 for src across sources nconc + (consult--multi-items idx src t))))) + +(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, +:initial-narrow, :add-history, :sort and :inherit-input-method. The other +options of `consult--read' are used by the `consult--multi' implementation +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. Sources which are nil are ignored. Source values +must be plists with the following fields. + +Either the :items or the :async source field is required: +* :items - List of strings to select from or function returning list of + strings. The strings can carry metadata in text properties, which is + then available to the :annotate, :action and :state functions. The + list can also consist of pairs, with the string in the `car' used for + display and the `cdr' the actual candidate. +* :async - Alternative to :items for asynchronous sources. The function + receives an asynchronous sink and an action as argument as documented + by `consult--async-pipeline'. + +Optional source fields: +* :name - Name of the source as a string, used for narrowing, + group titles and annotations. +* :narrow - Narrowing character, (char . string) pair or list of pairs. +* :category - Completion category symbol. +* :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)) + (collection (consult--multi-collection sources)) + (selected + (apply #'consult--read + collection + (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)) + +;;;; Customization macro + +(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 (warn "consult-customize: %s is neither a 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 `:command' or `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-args (options &rest defaults) + "Get configuration from `consult--customize-alist' for the current command. +OPTIONS is the option plist, and DEFAULTS are default options which are +overridden by OPTIONS." + (append + (mapcar (lambda (x) (eval x 'lexical)) + (alist-get (or (plist-get options :command) this-command) + consult--customize-alist)) + (consult--plist-remove '(:command) options) + defaults)) + +;;;; Commands + +;;;;; Command: consult-completion-in-region + +(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) + ;; Disable preview if anything odd is going on with the markers. + ;; Otherwise we get "Marker points into wrong buffer errors". See + ;; gh:minad/consult#375, where Org mode source blocks are + ;; completed in a different buffer than the original buffer. This + ;; completion is probably also problematic in my Corfu completion + ;; package. + (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)))) + ;; Use `add-face-text-property' on a copy of "cand in order to merge face properties + (setq cand (copy-sequence cand)) + (add-face-text-property 0 (length cand) 'consult-preview-insertion t cand) + ;; Use the `before-string' property since the overlay might be empty. + (overlay-put ov 'before-string cand))))))) + +(defun consult--in-region (start end table predicate) + "Internal `completion-in-region-function'. +The arguments START, END, TABLE and PREDICATE and +expected return value are as specified for `completion-in-region'." + (barf-if-buffer-read-only) + (let* ((initial (buffer-substring-no-properties start end)) + (metadata (completion-metadata initial table predicate)) + ;; bug#75910: category instead of `minibuffer-completing-file-name' + (minibuffer-completing-file-name + (eq 'file (completion-metadata-get metadata 'category))) + (threshold (completion--cycle-threshold metadata)) + (all (completion-all-completions initial table predicate + (if (<= start (point) end) + (- (point) start) + (length initial)) + metadata))) + ;; Normalize improper list + (when-let* ((last (last all))) + (setcdr last nil)) + (if (or (eq threshold t) (length< all (1+ (or threshold 1))) + (and completion-cycling completion-all-sorted-completions)) + (let (completion-auto-help) + (completion--in-region start end table predicate)) + ;; Wrap all annotation functions to ensure that they are executed + ;; in the original buffer. + (let* ((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))) + ;; Provide `:annotation-function' if `:company-docsig' is specified. + ,@(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)))))))) + (completion + (consult--local-let ((enable-recursive-minibuffers t)) + ;; Evaluate completion table in the original buffer. + ;; This is a reasonable thing to do and required by + ;; some completion tables in particular by lsp-mode. + ;; See gh:minad/vertico#61. + (consult--read + (consult--completion-table-in-buffer table) + :command #'consult-completion-in-region + :prompt (if (minibufferp) + ;; Use existing minibuffer prompt and input + (let ((prompt (buffer-substring (point-min) start))) + (put-text-property + (max 0 (1- (minibuffer-prompt-end))) (length prompt) + 'face 'shadow prompt) + prompt) + "Complete: ") + :state (consult--insertion-preview start end) + :predicate predicate + :initial initial)))) + (completion--replace start end completion) + (when exit-fun + (funcall exit-fun completion + ;; If completion is finished and cannot be further + ;; completed, return `finished'. Otherwise return + ;; `exact'. + (if (eq (try-completion completion table predicate) t) + 'finished 'exact))) + t)))) + +;;;###autoload +(defun consult-completion-in-region (start end table predicate) + "Use minibuffer completion as the UI for `completion-at-point'. + +The arguments START, END, TABLE and PREDICATE and expected return value +are as specified for `completion-in-region'. Use this function as a +value for `completion-in-region-function'." + (if (and (or (bound-and-true-p vertico-mode) (bound-and-true-p icomplete-mode)) + (not (eq table minibuffer-completion-table))) + (consult--in-region start end table predicate) + (completion--in-region start end table predicate))) + +;;;;; Command: consult-outline + +(defun consult--outline-candidates () + "Return list of outline heading strings with position attached." + (consult--forbid-minibuffer) + (let* ((line (line-number-at-pos (point-min) consult-line-numbers-widen)) + (heading-regexp (concat "^\\(?:" + ;; default definition from outline.el + (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 () ;; as in the default from outline.el + (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))) + (incf line (consult--count-lines (match-beginning 0))) + (push (consult--location-candidate + (buffer-substring-no-properties (pos-bol) (pos-eol)) + (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))) + +;;;###autoload +(defun consult-outline (&optional level) + "Jump to an outline heading, obtained by matching against `outline-regexp'. + +This command supports narrowing to a heading level and candidate +preview. The initial narrowing LEVEL can be given as prefix +argument. The symbol at point is added to the future history." + (interactive + (list (and current-prefix-arg (prefix-numeric-value current-prefix-arg)))) + (let* ((candidates (consult--slow-operation + "Collecting headings..." + (consult--outline-candidates))) + (min-level (- (cl-loop for cand in candidates minimize + (get-text-property 0 'consult--outline-level cand)) + ?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))) + (narrow-init (and level (max ?1 (min ?9 (+ level ?0)))))) + (consult--read + candidates + :prompt "Go to heading: " + :annotate (consult--line-fontify) + :category 'consult-location + :sort nil + :require-match t + :lookup #'consult--line-match + :initial-narrow narrow-init + :narrow (list :predicate narrow-pred :keys narrow-keys) + :history '(:input consult--line-history) + :add-history (thing-at-point 'symbol) + :state (consult--location-state candidates)))) + +;;;;; Command: consult-mark + +(defun consult--mark-candidates (markers) + "Return list of candidates strings for MARKERS." + (consult--forbid-minibuffer) + (let* ((candidates) + (width (length (number-to-string (line-number-at-pos + (point-max) + consult-line-numbers-widen)))) + (fmt (format #("%%%dd %%s%%s" 0 6 (face consult-line-number-prefix)) width))) + (save-excursion + (dolist (marker markers) + (when-let* ((pos (marker-position marker)) + ((and (eq (marker-buffer marker) (current-buffer)) + (consult--in-range-p pos)))) + (goto-char pos) + ;; `line-number-at-pos' is a very slow function, which should be + ;; replaced everywhere. However in this case the slow + ;; line-number-at-pos does not hurt much, since the mark ring is + ;; usually small since it is limited by `mark-ring-max'. + (let* ((line (line-number-at-pos pos consult-line-numbers-widen)) + (cand (format fmt line (consult--line-with-mark marker) (consult--tofu-encode marker)))) + (put-text-property 0 width 'consult-strip t cand) + (put-text-property 0 (length cand) 'consult-location (cons marker line) cand) + (push cand candidates))))) + (unless candidates + (user-error "No marks")) + (nreverse (delete-dups candidates)))) + +;;;###autoload +(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: " + :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))) + +;;;;; Command: consult-global-mark + +(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)) + ((not (minibufferp buf)))) + (with-current-buffer buf + (when (consult--in-range-p pos) + (goto-char pos) + ;; `line-number-at-pos' is slow, see comment in `consult--mark-candidates'. + (let* ((line (line-number-at-pos pos consult-line-numbers-widen)) + (prefix (consult--format-file-line-match (buffer-name buf) line "")) + (cand (concat prefix (consult--line-with-mark marker) (consult--tofu-encode marker)))) + (put-text-property 0 (length prefix) 'consult-strip t cand) + (put-text-property 0 (length cand) 'consult-location (cons marker line) cand) + (push cand candidates))))))) + (unless candidates + (user-error "No global marks")) + (nreverse (delete-dups candidates)))) + +;;;###autoload +(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: " + ;; Despite `consult-global-mark' formatting the candidates in grep-like + ;; style, we are not using the `consult-grep' category, since the candidates + ;; have location markers attached. + :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))) + +;;;;; Command: consult-line + +(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) + (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 + (buffer-substring-no-properties beg end) + (cons buffer beg) line line) + candidates) + (when (and (not default-cand) (>= line curr-line)) + (setq default-cand candidates))) + (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)))) + ;; Only create a new marker when jumping across buffers (for example + ;; `consult-line-multi'). Avoid creating unnecessary markers, when + ;; scrolling through candidates, since creating markers is not free. + (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)) + +;;;###autoload +(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-fontify curr-line) + :category 'consult-location + :sort nil + :require-match t + ;; Always add last `isearch-string' to future history + :add-history (list (thing-at-point 'symbol) isearch-string) + :history '(:input consult--line-history) + :lookup #'consult--line-match + :default (car candidates) + ;; Add `isearch-string' as initial input if starting from Isearch + :initial (or initial + (and isearch-mode + (prog1 isearch-string (isearch-done)))) + :state (consult--location-state candidates)))) + +;;;;; Command: consult-line-multi + +(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))) + (buf (if (consp marker) + (car marker) ;; Handle cheap marker + (marker-buffer marker)))) + (if buf (buffer-name buf) "Dead buffer")))) + +(defun consult--line-multi-candidates (buffers input callback) + "Collect matching candidates from multiple buffers. +INPUT is the user input which should be matched. +BUFFERS is the list of buffers. +CALLBACK receives the candidates." + (pcase-let ((`(,regexps . ,hl) (consult--compile-regexp input 'emacs completion-ignore-case)) + (candidates nil) + (cand-idx 0)) + (when regexps + (dolist (buf buffers) + (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))) + (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-*$")) + (cl-loop for r in (cdr regexps) always + (progn + (goto-char bol) + (re-search-forward r eol t)))) + (push (consult--location-candidate + (funcall hl (buffer-substring-no-properties bol eol)) + (cons buf bol) (1- line) cand-idx) + candidates) + (incf cand-idx)) + (goto-char (1+ eol))))))) + (funcall callback (nreverse candidates)) + (setq candidates nil))))) + +;;;###autoload +(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-fontify) + :category 'consult-location + :sort nil + :require-match t + ;; Always add last Isearch string to future history + :add-history (delq nil (list (thing-at-point 'symbol) isearch-string)) + :history '(:input consult--line-multi-history) + :lookup #'consult--line-multi-match + ;; Add `isearch-string' as initial input if starting from Isearch + :initial (or initial + (and isearch-mode + (prog1 isearch-string (isearch-done)))) + :state (consult--location-state (lambda () (funcall collection nil))) + :group #'consult--line-multi-group))) + +;;;;; Command: consult-keep-lines + +(defun consult--keep-lines-state (filter) + "State function for `consult-keep-lines' with FILTER function." + (let ((font-lock-orig font-lock-mode) + (whitespace-orig (bound-and-true-p whitespace-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 + ;; Use the same behavior as `keep-lines'. + (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))))) + ;; Font-locking is lazy, i.e., if a line has not been looked at yet, the + ;; line is not font-locked. Therefore we have to enforce slow font-locking + ;; now. In order to prevent is hang-up we check the region size against + ;; `consult-fontify-max-size'. + (when (< (- (point-max) (point-min)) consult-fontify-max-size) + (consult--fontify-region (point-min) (point-max))) + (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) + ;; Restoring content and point position + (when (and (eq action 'return) last-input) + ;; No undo recording, modification hooks, buffer modified-status + (with-silent-modifications (funcall replace content-orig point-orig))) + ;; Committing or new input provided -> Update + (when (and input ;; Input has been provided + (or + ;; Committing, but not with empty input + (and (eq action 'return) (not (string-match-p "\\`!? ?\\'" input))) + ;; Input has changed + (not (equal input last-input)))) + (let ((filtered-content + (if (string-match-p "\\`!? ?\\'" input) + ;; Special case the empty input for performance. + ;; Otherwise it could happen that the minibuffer is empty, + ;; but the buffer has not been updated. + content-orig + (if (eq action 'return) + (apply #'concat (mapcan (lambda (x) (list x "\n")) + (funcall filter input lines))) + (while-no-input + ;; Heavy computation is interruptible if *not* committing! + ;; Allocate new string candidates since the matching function mutates! + (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 whitespace-mode) (whitespace-mode -1)) + (when (bound-and-true-p hl-line-mode) (hl-line-mode -1)) + (if (eq action 'return) + (atomic-change-group + ;; Disable modification hooks for performance + (let ((inhibit-modification-hooks t)) + (funcall replace filtered-content))) + ;; No undo recording, modification hooks, buffer modified-status + (with-silent-modifications + (funcall replace filtered-content) + (setq last-input input)))))) + ;; Restore modes + (when (eq action 'return) + (when hl-line-orig (hl-line-mode 1)) + (when whitespace-orig (whitespace-mode 1)) + (when font-lock-orig (font-lock-mode 1)))))) + +;;;###autoload +(defun consult-keep-lines (filter &optional initial) + "Filter a subset of the lines in the current buffer with live preview. + +The filtered 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. If the buffer is narrowed to a region, the +command only acts on this region. See also `consult-focus-lines' which +uses overlays to display only matching lines, but does not modify the +buffer. + +FILTER is the filter function, called for each line. +INITIAL is the initial input." + (interactive + (list (lambda (pattern cands) + ;; Use consult-location completion category when filtering lines + (consult--completion-filter-dispatch + pattern cands 'consult-location 'highlight)))) + (consult--forbid-minibuffer) + (let ((ro buffer-read-only)) + (unwind-protect + (minibuffer-with-setup-hook + (lambda () + (when ro + (consult--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--line-history + :state (consult--keep-lines-state filter)))) + (setq buffer-read-only ro)))) + +;;;;; Command: consult-focus-lines + +(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 + (when (use-region-p) + (narrow-to-region + (region-beginning) + ;; Behave the same as `keep-lines'. + ;; Move to the next line. + (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 + ;; Use "\n" for empty lines, since we need a non-empty string to + ;; attach the text property to. + (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 (incf i) beg) line) + (push line lines))) + (setq lines (nreverse lines))))) + (lambda (action input) + ;; New input provided -> Update + (when (and input (not (equal input last-input))) + (let (new-overlays) + (pcase (while-no-input + (unless (string-match-p "\\`!? ?\\'" input) ;; Empty input. + (let* ((inhibit-quit (eq action 'return)) ;; Non interruptible, when quitting! + (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) + ;; Check for empty lines, see above. + 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 nil 'show) + (goto-char pt-orig)) + (t + ;; Successfully terminated -> Remember invisible overlays + (cl-callf nconc consult--focus-lines-overlays overlays) + ;; move point past invisible + (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)))))))) + +;;;###autoload +(defun consult-focus-lines (filter &optional show initial) + "Show only matching lines using overlays. + +In contrast to `consult-keep-lines' the buffer is not modified. The +FILTER selects the lines which are shown. 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 +rerun the command and exit the minibuffer directly without input to +reveal the lines. When called from Elisp, the filtering is performed by +a FILTER function. If the buffer is narrowed to a region, the command +only acts on this region. + +FILTER is the filter function, called for each line. +SHOW is the prefix argument, if non-nil reveal all hidden lines. +INITIAL is the initial input." + (interactive + (list (lambda (pattern cands) + ;; Use consult-location completion category when filtering lines + (consult--completion-filter-dispatch + pattern cands 'consult-location nil)) + current-prefix-arg)) + (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--line-history + :state (consult--focus-lines-state filter)))) + (cl-callf2 assq-delete-all 'consult--focus-lines-overlays mode-line-misc-info) + (when (and consult--focus-lines-overlays consult--focus-lines-indicator) + (push `(consult--focus-lines-overlays ,consult--focus-lines-indicator) + mode-line-misc-info))) + +;;;;; Command: consult-goto-line + +(defun consult--goto-line-position (str msg) + "Transform input STR to line number. +Print an error message with MSG function." + (save-match-data + (if (and str (string-match "\\`\\([[:digit:]]+\\):?\\([[:digit:]]*\\)\\'" str)) + (let ((line (string-to-number (match-string 1 str))) + (col (string-to-number (match-string 2 str)))) + (save-excursion + (save-restriction + (when consult-line-numbers-widen + (widen)) + (goto-char (point-min)) + (forward-line (1- line)) + (goto-char (min (+ (point) col) (pos-eol))) + (point)))) + (when (and str (not (equal str ""))) + (funcall msg "Please enter a number.")) + nil))) + +;;;###autoload +(defun consult-goto-line (&optional arg) + "Read line number and jump to the line with preview. + +Enter either a line number to jump to the first column of the +given line or line:column in order to jump to a specific column. +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))))) + #'consult--minibuffer-message))) + (consult--jump pos) + t))))) + +;;;;; Command: consult-recent-file + +(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'." + ;; Try to preserve the buffer as is, if it has already been opened, for + ;; example in literal or raw mode. + (setq file (abbreviate-file-name (expand-file-name file))) + (consult--buffer-action (or (get-file-buffer file) (find-file-noselect file)))) + +(consult--define-state file) + +;;;###autoload +(defun consult-recent-file () + "Find recent file using `completing-read'." + (interactive) + (find-file + (consult--read + (or + (mapcar #'consult--fast-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))) + +;;;;; Command: consult-mode-command + +(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* ((case-fold-search) + (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)) + (or (not read-extended-command-predicate) + (funcall read-extended-command-predicate sym buffer))) + (let ((name (symbol-name sym))) + (unless (string-match-p command-filter name) + (push (propertize name + 'consult--candidate sym + 'consult--type key) + commands)))))))))))) + +;;;###autoload +(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)))) + +;;;;; Command: consult-yank + +(defun consult--read-from-kill-ring () + "Open kill ring menu and return selected string." + ;; `current-kill' updates `kill-ring' with interprogram paste, see + ;; gh:minad/consult#443. + (current-kill 0) + ;; Do not specify a :lookup function in order to preserve completion-styles + ;; highlighting of the current candidate. We have to perform a final lookup to + ;; obtain the original candidate which may be propertized with yank-specific + ;; properties, like 'yank-handler. + (consult--lookup-member + (consult--read + (consult--remove-dups + (or (if yank-from-kill-ring-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 ;; disable history + :sort nil + :category 'kill-ring + :require-match t + :lookup #'consult--lookup-member + :state + (consult--insertion-preview + (point) + ;; If previous command is yank, hide previously yanked string + (or (and (eq last-command 'yank) (mark t)) (point)))) + kill-ring)) + +;; Adapted from the Emacs `yank-from-kill-ring' function. +;;;###autoload +(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', 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 yank-from-kill-ring-rotate + (if-let* ((pos (seq-position kill-ring string))) + (setq kill-ring-yank-pointer (nthcdr pos kill-ring)) + (kill-new string))) + (when (consp arg) + ;; Swap point and mark like in `yank'. + (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) + +;;;###autoload +(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', 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))) + +;; Adapted from the Emacs yank-pop function. +;;;###autoload +(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." + (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))))))))) + +;;;;; Command: consult-bookmark + +(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 + ;; Only preview bookmarks with the default handler. + (when-let* ((bm (and cand (eq action 'preview) (assoc cand bookmark-alist))) + (handler (or (bookmark-get-handler bm) #'bookmark-default-handler)) + ((eq handler #'bookmark-default-handler)) + (file (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 (cl-loop for (y _ . xs) in consult-bookmark-narrow nconc + (cl-loop for x in xs collect (cons x y))))) + (cl-loop for bm in bookmark-alist collect + (propertize (car bm) + 'consult--type + (alist-get + (or (bookmark-get-handler bm) #'bookmark-default-handler) + narrow))))) + +;;;###autoload +(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 (cl-loop for (x y . _) in consult-bookmark-narrow collect (cons x y)))) + (consult--read + (consult--bookmark-candidates) + :prompt "Bookmark: " + :state (consult--bookmark-preview) + :category 'bookmark + :history 'bookmark-history + ;; Add default names to future history. + ;; Ignore errors such that `consult-bookmark' can be used in + ;; buffers which are not backed by a file. + :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))) + +;;;;; Command: consult-complex-command + +;;;###autoload +(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 ;; disable history + :category 'expression)))) + ;; Taken from `repeat-complex-command' + (add-to-history 'command-history cmd) + (apply #'funcall-interactively + (car cmd) + (mapcar (lambda (e) (eval e t)) (cdr cmd))))) + +;;;;; Command: consult-history + +(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 + ;; In the minibuffer we use the current minibuffer history, + ;; which can be configured by setting `minibuffer-history-variable'. + ((minibufferp) + (when (eq minibuffer-history-variable t) + (user-error "Minibuffer history is disabled for `%s'" this-command)) + (list (mapcar #'consult--tofu-strip + (if (eq minibuffer-history-variable 'command-history) + ;; If pressing "C-x M-:", i.e., `repeat-complex-command', + ;; we are instead querying the `command-history' and get a + ;; full s-expression. Alternatively you might want to use + ;; `consult-complex-command', which can also be bound to + ;; "C-x M-:"! + (mapcar #'prin1-to-string command-history) + (symbol-value minibuffer-history-variable))))) + ;; Otherwise we use a mode-specific history, see `consult-mode-histories'. + (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)))))) + +;;;###autoload +(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) + (declare-function ring-elements "ring") + (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 ;; disable history + :category ;; Report category depending on history variable + (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) + :lookup #'consult--lookup-member + :state (consult--insertion-preview beg end))))) + (delete-region beg end) + (when index + (set index (seq-position history str))) + (insert (substring-no-properties str)))) + +;;;;; Command: consult-isearch-history + +(defun consult-isearch-forward (&optional reverse) + "Continue Isearch forward optionally in REVERSE." + (declare (completion ignore)) + (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." + (declare (completion ignore)) + (interactive) + (consult-isearch-forward (not reverse))) + +(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." + ;; Do not throw an error on empty history, in order to allow starting a + ;; search. We do not :require-match here. + (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) + ;; The search type can be distinguished via text properties. + (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)))) + ;; Disambiguate history items. The same string could + ;; occur with different search types. + (consult--tofu-append cand type))) + history)))) + +(defconst consult--isearch-history-narrow + '((?c . "Char") + (?u . "Custom") + (?l . "Literal") + (?r . "Regexp") + (?s . "Symbol") + (?w . "Word"))) + +;;;###autoload +(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) ;; Avoid cursor flickering + (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 + :history t ;; disable history + :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 ""))) + ;; Setting `isearch-regexp' etc only works outside of `with-isearch-suspended'. + (unless (plist-member (text-properties-at 0 isearch-string) 'isearch-regexp-function) + (setq isearch-regexp t + isearch-regexp-function nil)))) + +;;;;; Command: consult-minor-mode-menu + +(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 + ;; according to describe-minor-mode-completion-table-for-symbol + ;; the minor-mode-list contains *all* minor modes + (mapcar (lambda (sym) (cons (symbol-name sym) sym)) minor-mode-list) + ;; take the lighters from minor-mode-alist + (delq nil + (mapcar (pcase-lambda (`(,sym ,lighter)) + (when (and lighter (not (equal "" lighter))) + (let (message-log-max) + (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"))) + +;;;###autoload +(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))) + +;;;;; Command: consult-theme + +;;;###autoload +(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) + (with-selected-window (or (active-minibuffer-window) + (selected-window)) + (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 + (unless (and (memq theme custom-known-themes) (get theme 'theme-settings)) + (load-theme theme 'no-confirm 'no-enable)) + (if (and (memq theme custom-known-themes) (get theme 'theme-settings)) + (enable-theme theme) + (consult--minibuffer-message "%s is not a valid theme" theme))))) + +;;;;; Command: consult-buffer + +(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 ((current (car (memq (current-buffer) buffers))) visible) + (consult--keep! buffers + (unless (eq it current) + (if (get-buffer-window it 'visible) + (progn (push it visible) nil) + it))) + (nconc buffers (nreverse visible) (and current (list current))))) + +(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) + "Return a list of buffers and create an appropriate prompt string. +Return a pair of a prompt string and a list of buffers. PROMPT +is the prefix of the prompt string. QUERY specifies the buffers +to search and 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--left-truncate-file ndir))) + (t ""))) + buffers))) + +(defun consult--frame-buffer-list () + "List of buffers belonging to the current frame or tab." + (let ((buffers (append (frame-parameter nil 'buffer-list) + (reverse (frame-parameter nil 'buried-buffer-list))))) + ;; Sometimes visible buffers are not registered in the buffer-list. + (cl-loop for win in (window-list) for buf = (window-buffer win) + unless (memq buf buffers) do (push buf buffers)) + buffers)) + +(cl-defun consult--buffer-query ( &key sort directory mode as predicate (filter t) + include (exclude consult-buffer-filter) + (buffer-list consult-buffer-list-function)) + "Query for a list of matching buffers. +The function supports filtering by various criteria which are +used throughout Consult. In particular it is the backbone of +most `consult-buffer-sources'. +DIRECTORY can either be the symbol project or a file name. +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. +BUFFER-LIST is a function or a list of buffers. +AS is a conversion function." + (let ((root (consult--normalize-directory directory))) + (setq buffer-list (cond + ((functionp buffer-list) (funcall buffer-list)) + ((listp buffer-list) (copy-sequence buffer-list)) + (t (buffer-list)))) + (when (or filter mode root) + (let ((exclude-re (consult--regexp-filter exclude)) + (include-re (consult--regexp-filter include)) + (case-fold-search)) + (consult--keep! buffer-list + (and + (or (not mode) + (let ((mm (buffer-local-value 'major-mode it))) + (if (consp mode) + (seq-some (lambda (m) (provided-mode-derived-p mm m)) mode) + (provided-mode-derived-p mm 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)) + it)))) + (when sort + (setq buffer-list (funcall (intern (format "consult--buffer-sort-%s" sort)) buffer-list))) + (when as + (cl-loop for it in-ref buffer-list do (setf it (funcall as it)))) + buffer-list)) + +(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-pair (buffer) + "Return a pair of name of BUFFER and BUFFER." + (cons (buffer-name buffer) buffer)) + +(defun consult--buffer-preview () + "Buffer preview function." + (let ((orig-buf (window-buffer (consult--original-window))) + (orig-prev (copy-sequence (window-prev-buffers))) + (orig-next (copy-sequence (window-next-buffers))) + (orig-bl (copy-sequence (frame-parameter nil 'buffer-list))) + (orig-bbl (copy-sequence (frame-parameter nil 'buried-buffer-list))) + other-win) + (lambda (action cand) + (pcase action + ('return + ;; Restore buffer list for the current tab + (set-frame-parameter nil 'buffer-list orig-bl) + (set-frame-parameter nil 'buried-buffer-list orig-bbl)) + ('exit + (set-window-prev-buffers other-win orig-prev) + (set-window-next-buffers other-win orig-next)) + ('preview + ;; Prevent opening the preview in another tab, since restoring the tab + ;; status is difficult and also costly. + (cl-letf* (((symbol-function #'display-buffer-in-tab) #'ignore) + ((symbol-function #'display-buffer-in-new-tab) #'ignore)) + (when (and (eq consult--buffer-display #'switch-to-buffer-other-window) + (not other-win)) + (switch-to-buffer-other-window orig-buf 'norecord) + (setq other-win (selected-window))) + (let ((win (or other-win (selected-window))) + (buf (or (and cand (get-buffer cand)) orig-buf))) + (when (and (window-live-p win) (buffer-live-p buf) + (not (buffer-match-p consult-preview-excluded-buffers buf))) + (with-selected-window win + (unless (or orig-prev orig-next) + (setq orig-prev (copy-sequence (window-prev-buffers)) + orig-next (copy-sequence (window-next-buffers)))) + (switch-to-buffer 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 source for `consult-buffer'.") + +(defvar consult-source-project-buffer + `( :name "Project Buffer" + :narrow ?b + :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 #'consult--buffer-pair)))) + "Project buffer source for `consult-buffer'.") + +(defvar consult-source-project-recent-file + `( :name "Project File" + :narrow ?f + :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)) + items) + (dolist (file (bound-and-true-p recentf-list) (nreverse items)) + ;; Emacs 29 abbreviates file paths by default, see + ;; `recentf-filename-handlers'. I recommend to set + ;; `recentf-filename-handlers' to nil to avoid any slow down. + (unless (eq (aref file 0) ?/) + (let (file-name-handler-alist) ;; No Tramp slowdown please. + (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 "./")) + (push (cons part file) items)))))))) + "Project file source for `consult-buffer'.") + +(defvar consult-source-project-root + `( :name "Project Root" + :narrow ?r + :category file + :face consult-file + :history file-name-history + :action ,(lambda (root) + (let ((default-directory root)) + (call-interactively #'find-file))) + :items ,#'consult--project-known-roots) + "Known project root source.") + +(defvar consult-source-project-buffer-hidden + `( :hidden t :narrow ((?p . "Project") (?B . "Project Buffer")) + ,@consult-source-project-buffer) + "Like `consult-source-project-buffer' but hidden by default.") + +(defvar consult-source-project-recent-file-hidden + `( :hidden t :narrow ((?p . "Project") (?F . "Project File")) + ,@consult-source-project-recent-file) + "Like `consult-source-project-recent-file' but hidden by default.") + +(defvar consult-source-project-root-hidden + `( :hidden t :narrow ((?p . "Project") (?R . "Project Root")) + ,@consult-source-project-root) + "Like `consult-source-project-root' but hidden by default.") + +(defvar consult-source-hidden-buffer + `( :name "Hidden Buffer" + :narrow ?\s + :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 #'consult--buffer-pair + :buffer-list t))) + "Hidden buffer source for `consult-buffer'. +The source is hidden by default and can be summoned via its narrow key. +All buffers are taken into account, i.e., the entire `buffer-list' from +all frames.") + +(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 #'consult--buffer-pair + :predicate + (lambda (buf) + (and (buffer-modified-p buf) + (buffer-file-name buf)))))) + "Modified buffer source for `consult-buffer'. +The source is hidden by default and can be summoned via its narrow key. +Only buffers returned by the `consult-buffer-list-function' are taken +into account.") + +(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 #'consult--buffer-pair))) + "Buffer source for `consult-buffer'. +Only buffers returned by the `consult-buffer-list-function' are taken into +account.") + +(defvar consult-source-other-buffer + `( :name "Other Buffer" + :narrow ?o + :hidden t + :category buffer + :face consult-buffer + :history buffer-name-history + :state ,#'consult--buffer-state + :enabled ,(lambda () (not (eq consult-buffer-list-function #'buffer-list))) + :items + ,(lambda () + (let ((local (consult--string-hash (consult--buffer-query)))) + (consult--buffer-query :sort 'visibility + :predicate (lambda (buf) (not (gethash buf local))) + :as #'consult--buffer-pair + :buffer-list t)))) + "Source for `consult-buffer' for buffers from other frames or tabs. +The source is hidden by default and can be summoned via its narrow key. +Only buffers returned by the `consult-buffer-list-function' are taken +into account.") + +(autoload 'consult-register--candidates "consult-register") + +(defun consult--buffer-register-p (reg) + "Return non-nil if REG is a buffer register." + (and (eq (car-safe reg) 'buffer) (buffer-live-p (get-buffer (cdr reg))))) + +(defvar consult-source-buffer-register + `( :name "Buffer Register" + :narrow (?r . "Register") + :category buffer + :state ,#'consult--buffer-state + :enabled ,(lambda () (cl-loop for (_ . reg) in register-alist + thereis (consult--buffer-register-p reg))) + :items ,(lambda () (consult-register--candidates #'consult--buffer-register-p))) + "Buffer register source.") + +(defun consult--file-register-p (reg) + "Return non-nil if REG is a file register." + (memq (car-safe reg) '(file-query file))) + +(defvar consult-source-file-register + `( :name "File Register" + :narrow (?r . "Register") + :category file + :state ,#'consult--file-state + :enabled ,(lambda () (cl-loop for (_ . reg) in register-alist + thereis (consult--file-register-p reg))) + :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)) + items) + (dolist (file (bound-and-true-p recentf-list) (nreverse items)) + ;; Emacs 29 abbreviates file paths by default, see + ;; `recentf-filename-handlers'. I recommend to set + ;; `recentf-filename-handlers' to nil to avoid any slow down. + (unless (eq (aref file 0) ?/) + (let (file-name-handler-alist) ;; No Tramp slowdown please. + (setq file (expand-file-name file)))) + (unless (gethash file ht) + (push (consult--fast-abbreviate-file-name file) items)))))) + "Recent file source for `consult-buffer'.") + +;;;###autoload +(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))) + ;; For non-matching candidates, fall back to buffer creation. + (unless (plist-get (cdr selected) :match) + (consult--buffer-action (car selected))))) + +(defmacro consult--with-project (&rest body) + "Ensure that BODY is executed with a project root." + (declare (indent 0) (debug t)) + `(consult--with-project-f (lambda () ,@body))) + +(defun consult--with-project-f (body) + "See `consult--with-project' for documentation." + ;; We have to work quite hard here to ensure that the project root is only + ;; overridden at the current recursion level. When entering a recursive + ;; minibuffer session, we should be able to still switch the project. + (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)))))) + (funcall body))) + +;;;###autoload +(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))) + +;;;###autoload +(defun consult-buffer-other-window () + "Variant of `consult-buffer', switching to a buffer in another window." + (interactive) + (let ((consult--buffer-display #'switch-to-buffer-other-window)) + (consult-buffer))) + +;;;###autoload +(defun consult-buffer-other-frame () + "Variant of `consult-buffer', switching to a buffer in another frame." + (interactive) + (let ((consult--buffer-display #'switch-to-buffer-other-frame)) + (consult-buffer))) + +;;;###autoload +(defun consult-buffer-other-tab () + "Variant of `consult-buffer', switching to a buffer in another tab." + (interactive) + (let ((consult--buffer-display #'switch-to-buffer-other-tab)) + (consult-buffer))) + +;;;;; Command: consult-grep + +(defun consult--grep-format (builder) + "Async function highlighting grep match results. +BUILDER is the command line builder function." + (consult--async-transform-by-input + (lambda (input) + (let ((highlight (cdr (funcall builder input)))) + (lambda (cands) + (let ((file "") (file-len 0) result) + (save-match-data + (dolist (str cands (nreverse result)) + (when (string-match consult--grep-match-regexp str) + ;; We share the file name across candidates to reduce + ;; the amount of allocated memory. + (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 (and consult-grep-max-columns + (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)) + ;; Store file name in order to avoid allocations in `consult--prefix-group' + (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))))))))))) + +(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-noselect'." + (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 #'consult--file-action) 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 variable `grep-find-ignored-directories' and the variable +`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 initial +input." + (pcase-let* ((`(,prompt ,paths ,dir) (consult--directory-prompt prompt dir)) + (default-directory dir) + (builder (funcall make-builder paths))) + (consult--read + (consult--process-collection builder + :transform (consult--grep-format builder) + :file-handler t) + :prompt prompt + :lookup #'consult--lookup-member + :state (consult--grep-state) + :initial initial + :add-history (thing-at-point '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 look-ahead." + (eq 0 (process-file-shell-command + (concat "echo xaxbx | " + (mapconcat #'shell-quote-argument `(,@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-literals arg ignore-case)) + (pcase-let ((`(,re . ,hl) (consult--compile-regexp arg type ignore-case))) + (when re + (cons (append cmd + (list (if (eq type 'pcre) "-P" "-E") ;; perl or extended + "-e" (consult--join-regexps re type)) + opts paths) + hl)))))))) + +(autoload 'consult-compile-error "consult-compile") + +;;;###autoload +(defun consult-grep-match (&optional arg) + "Jump to grep matches related to the current project or file. + +This command collects entries from all related Grep buffers. The +command supports preview of the currently selected match. With prefix +ARG, jump to the match in the Grep buffer, instead of to the actual +location of the match. This command is a thin wrapper around +`consult-compile-error'." + (interactive "P") + (consult-compile-error arg t)) + +;;;###autoload +(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 or files 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. If the command is invoked with a +double prefix argument (twice `C-u') the user is asked for a project, if +not yet inside a project, or the current project 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-opt#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)) + +;;;;; Command: consult-git-grep + +(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-literals arg ignore-case)) + (pcase-let ((`(,re . ,hl) (consult--compile-regexp arg 'extended ignore-case))) + (when re + (cons (append cmd + (cdr (mapcan (lambda (x) (list "--and" "-e" x)) re)) + opts paths) + hl)))))))) + +;;;###autoload +(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)) + +;;;;; Command: consult-ripgrep + +(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 + (and (not (or (member "-s" flags) (member "--case-sensitive" flags))) + (or (member "-i" flags) (member "--ignore-case" flags) + (and (or (member "-S" flags) (member "--smart-case" flags)) + (let (case-fold-search) + ;; Case insensitive if there are no uppercase letters + (not (string-match-p "[[:upper:]]" arg)))))))) + (if (or (member "-F" flags) (member "--fixed-strings" flags)) + (cons (append cmd (list "-e" arg) opts paths) + (apply-partially #'consult--highlight-literals arg ignore-case)) + (pcase-let ((`(,re . ,hl) (consult--compile-regexp arg type ignore-case))) + (when re + (cons (append cmd (and (eq type 'pcre) '("-P")) + (list "-e" (consult--join-regexps re type)) + opts paths) + hl)))))))) + +;;;###autoload +(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)) + +;;;;; Command: consult-find + +(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 initial input." + (consult--read + (consult--process-collection builder + :transform (consult--async-map (lambda (x) (string-remove-prefix "./" x))) + :highlight t :file-handler t) ;; allow tramp + :prompt prompt + :sort nil + :require-match t + :initial initial + :add-history (thing-at-point 'filename) + :category 'file + :history '(:input consult--find-history))) + +(defun consult--find-make-builder (paths) + "Build find command line, finding across PATHS." + (let* ((cmd (seq-mapcat (lambda (x) + (if (equal x ".") paths (list x))) + (consult--build-args consult-find-args))) + (type (if (eq 0 (process-file-shell-command + (concat (car cmd) " -regextype emacs -version"))) + 'emacs 'basic))) + (lambda (input) + (pcase-let* ((`(,arg . ,opts) (consult--command-split input)) + (method (or (seq-find (lambda (o) + (member o '("-name" "-path" "-regex" + "-iname" "-ipath" "-iregex"))) + opts) + "-iregex")) + (opts (remove method opts)) + (ignore-case (string-prefix-p "-i" method))) + (if (not (string-suffix-p "regex" method)) + (when-let* ((args (consult--split-escaped arg))) + (cons (append cmd + (cdr (mapcan + (lambda (x) `("-and" ,method ,(format "*%s*" x))) + args)) + opts) + (apply-partially #'consult--highlight-literals args ignore-case))) + (pcase-let ((`(,re . ,hl) (consult--compile-regexp arg type ignore-case))) + (when (or re opts) ;; Either option or regexp must be provided + (cons (append cmd + (cdr (mapcan + (lambda (x) + `("-and" ,method + ,(format + ".*%s.*" + ;; Replace non-capturing groups with capturing groups. + ;; GNU find does not support non-capturing groups. + (replace-regexp-in-string + "\\\\(\\?:" "\\(" x 'fixedcase 'literal)))) + re)) + opts) + hl)))))))) + +;;;###autoload +(defun consult-find (&optional dir initial) + "Search for files with `find' in DIR. +The file names must match the input regexp. INITIAL is the +initial minibuffer 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)))) + +;;;;; Command: consult-fd + +(defun consult--fd-make-builder (paths) + "Build find command line, finding across PATHS." + (let ((cmd (consult--build-args consult-fd-args))) + (lambda (input) + (pcase-let* ((`(,arg . ,opts) (consult--command-split input)) + (flags (append cmd opts)) + (ignore-case + (and (not (or (member "-s" flags) (member "--case-sensitive" flags))) + (or (member "-i" flags) (member "--ignore-case" flags) + (let (case-fold-search) + ;; Case insensitive if there are no uppercase letters + (not (string-match-p "[[:upper:]]" arg))))))) + (if (or (member "-F" flags) (member "--fixed-strings" flags) + (member "-g" flags) (member "--glob" flags)) + (when-let* ((args (consult--split-escaped arg))) + (cons (append cmd opts + (mapcan (lambda (x) `("--and" ,x)) + (if (or (member "-g" flags) (member "--glob" flags)) + (mapcar (lambda (x) (concat "**/" x)) args) + args)) + (mapcan (lambda (x) `("--search-path" ,x)) paths)) + (apply-partially #'consult--highlight-literals args ignore-case))) + (pcase-let ((`(,re . ,hl) (consult--compile-regexp arg 'pcre ignore-case))) + (when (or re opts) ;; Either option or regexp must be provided + (cons (append cmd opts + (mapcan (lambda (x) `("--and" ,x)) re) + (mapcan (lambda (x) `("--search-path" ,x)) paths)) + hl)))))))) + +;;;###autoload +(defun consult-fd (&optional dir initial) + "Search for files with `fd' in DIR. +The file names must match the input regexp. INITIAL is the +initial minibuffer input. See `consult-grep' for details +regarding the asynchronous search and the arguments." + (interactive "P") + (pcase-let* ((`(,prompt ,paths ,dir) (consult--directory-prompt "Fd" dir)) + (default-directory dir) + (builder (consult--fd-make-builder paths))) + (find-file (consult--find prompt builder initial)))) + +;;;;; Command: consult-locate + +(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) + (consult--split-escaped arg) opts) + (cdr (consult--default-regexp-compiler arg 'basic t)))))) + +;;;###autoload +(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))) + +;;;;; Command: consult-man + +(defun consult--man-builder (input) + "Build command line from INPUT." + (pcase-let* ((`(,arg . ,opts) (consult--command-split input)) + (`(,re . ,hl) (consult--compile-regexp arg 'extended t))) + (when re + (cons (append (consult--build-args consult-man-args) + (list (consult--join-regexps re 'extended)) + 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-preview () + "Create preview function for man pages." + (let ((preview (consult--buffer-preview)) + (orig (buffer-list)) + buffers) + (lambda (action cand) + (unless cand + (pcase-dolist (`(,_ . ,buf) buffers) + (kill-buffer buf)) + (setq buffers nil)) + (let ((consult--buffer-display #'switch-to-buffer-other-window)) + (funcall preview action + (and cand + (eq action 'preview) + (or (cdr (assoc cand buffers)) + (when-let* ((buf (consult--man-action cand t))) + (unless (memq buf orig) + (cl-callf consult--preview-add-buffer + buffers (cons cand buf))) + buf)))))))) + +(defun consult--man-action (page &optional nodisplay) + "Create man PAGE buffer, do not display if NODISPLAY is non-nil." + (dlet ((Man-prefer-synchronous-call t) + (Man-notify-method (and (not nodisplay) 'aggressive)) + (inhibit-message t) + (message-log-max nil)) + (when-let* ((buf (man page)) + ((buffer-live-p buf))) + (with-current-buffer buf + (goto-char (point-min)) + (current-buffer))))) + +(consult--define-state man) + +;;;###autoload +(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) + (consult--read + (consult--process-collection #'consult--man-builder + :transform (consult--async-transform #'consult--man-format) + :highlight t) + :prompt "Manual entry: " + :require-match t + :category 'consult-man + :state (consult--man-state) + :lookup (apply-partially #'consult--lookup-prop 'consult-man) + :initial initial + :add-history (thing-at-point 'symbol) + :history '(:input consult--man-history))) + +;;;; Integration with completion systems + +;;;;; Integration: Default *Completions* + +(defun consult--default-completion-list-preview () + "Preview candidate at point in *Completions* buffer." + (when-let* ((win (active-minibuffer-window)) + (buf (window-buffer win)) + (fun (buffer-local-value 'consult--preview-function buf))) + (funcall fun))) + +(defun consult--default-completion-list-preview-setup () + "Setup preview at point in *Completions* buffer." + (add-hook 'post-command-hook #'consult--default-completion-list-preview nil 'local)) +(add-hook 'completion-list-mode-hook #'consult--default-completion-list-preview-setup) + +(defun consult--default-completion-minibuffer-candidate () + "Return current minibuffer candidate from default completion system or Icomplete." + (when (minibufferp) + (let ((content (minibuffer-contents-no-properties))) + ;; When the current minibuffer content matches a candidate, return it! + (if (test-completion content + minibuffer-completion-table + minibuffer-completion-predicate) + content + ;; Return the full first candidate of the sorted completion list. + (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." + (when-let* ((buffer + (if (derived-mode-p #'completion-list-mode) + ;; Use current buffer if already inside *Completions* buffer + (current-buffer) + ;; Otherwise check if there is an active *Completions* buffer + ;; which can be controlled remotely from the minibuffer. See + ;; the setting `minibuffer-visible-completions'. + (when-let* ((bound-and-true-p minibuffer-visible-completions) + (window (get-buffer-window "*Completions*" 'visible)) + (buffer (window-buffer window)) + ((eq (buffer-local-value 'completion-reference-buffer buffer) + (window-buffer (active-minibuffer-window))))) + buffer)))) + (with-current-buffer buffer + ;; TODO Use `completion-list-candidate-at-point' on Emacs 31 + (let (beg) + (when (cond + ((and (not (eobp)) (get-text-property (point) 'completion--string)) + (setq beg (1+ (point)))) + ((and (not (bobp)) (get-text-property (1- (point)) 'completion--string)) + (setq beg (point)))) + (get-text-property (previous-single-property-change beg 'completion--string) + 'completion--string)))))) + +(defun consult--default-completion-list-refresh () + "Refresh default completion UI." + (when (and (bound-and-true-p completion-eager-update) + (bound-and-true-p completion-eager-display) + (not (bound-and-true-p vertico-mode)) + (not (bound-and-true-p icomplete-mode))) + (minibuffer-completion-help))) + +;;;;; Integration: Vertico + +(defvar vertico--input) + +(defun consult--vertico-candidate () + "Return current candidate for Consult preview." + (declare-function vertico--candidate "ext:vertico") + (and vertico--input (vertico--candidate 'highlight))) + +(defun consult--vertico-refresh () + "Refresh completion UI." + (declare-function vertico--exhibit "ext:vertico") + (when vertico--input + (setq vertico--input t) + (vertico--exhibit))) + +(with-eval-after-load 'vertico + (add-hook 'consult--completion-candidate-hook #'consult--vertico-candidate) + (add-hook 'consult--completion-refresh-hook #'consult--vertico-refresh) + (define-key consult-async-map [remap vertico-insert] 'vertico-next-group)) + +;;;;; Integration: Mct + +(with-eval-after-load 'mct + (add-hook 'consult--completion-refresh-hook 'mct--live-completions-refresh)) + +;;;;; Integration: Icomplete + +(defun consult--icomplete-refresh () + "Refresh icomplete view." + (defvar icomplete-mode) + (declare-function icomplete-exhibit "icomplete") + (when icomplete-mode + (let ((top (car completion-all-sorted-completions))) + (completion--flush-all-sorted-completions) + ;; force flushing, otherwise narrowing is broken! + (setq completion-all-sorted-completions nil) + (when top + (let* ((completions (completion-all-sorted-completions)) + (last (last completions)) + (before)) ;; completions before top + ;; warning: completions is an improper list + (while (consp completions) + (if (equal (car completions) top) + (progn + (setcdr last (append (nreverse before) (cdr last))) + (setq completion-all-sorted-completions completions + completions nil)) + (push (car completions) before) + (setq completions (cdr completions))))))) + (icomplete-exhibit))) + +(with-eval-after-load 'icomplete + (add-hook 'consult--completion-refresh-hook #'consult--icomplete-refresh)) + +(provide 'consult) +;;; consult.el ends here diff --git a/.config/emacs/lisp/minadstack/corfu-history.el b/.config/emacs/lisp/minadstack/corfu-history.el new file mode 100644 index 0000000..ba7af47 --- /dev/null +++ b/.config/emacs/lisp/minadstack/corfu-history.el @@ -0,0 +1,114 @@ +;;; corfu-history.el --- Sorting by history for Corfu -*- lexical-binding: t -*- + +;; Copyright (C) 2022-2026 Free Software Foundation, Inc. + +;; Author: Daniel Mendler <mail@daniel-mendler.de> +;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> +;; Created: 2022 +;; Version: 2.10 +;; Package-Requires: ((emacs "29.1") (compat "31") (corfu "2.10")) +;; URL: https://github.com/minad/corfu + +;; This file is part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Enable `corfu-history-mode' to sort candidates by their history position. +;; The recently selected candidates are stored in the `corfu-history' variable. +;; If `history-delete-duplicates' is nil, duplicate elements are ranked higher +;; with exponential decay. In order to save the history across Emacs sessions, +;; enable `savehist-mode'. +;; +;; (corfu-history-mode) +;; (savehist-mode) + +;;; Code: + +(require 'corfu) +(eval-when-compile + (require 'cl-lib)) + +(defvar corfu-history nil + "History of Corfu candidates. +The maximum length is determined by the variable `history-length' +or the property `history-length' of `corfu-history'.") + +(defvar corfu-history--hash nil + "Hash table of Corfu candidates.") + +(defcustom corfu-history-duplicate 10 + "History position shift for duplicate history elements. +The more often a duplicate element occurs in the history, the earlier it +appears in the completion list. The shift decays exponentially with +`corfu-history-decay'. Note that duplicates occur only if +`history-delete-duplicates' is disabled." + :type 'number + :group 'corfu) + +(defcustom corfu-history-decay 10 + "Exponential decay for the position shift of duplicate elements. +The shift will decay away after `corfu-history-duplicate' times +`corfu-history-decay' history elements." + :type 'number + :group 'corfu) + +(defun corfu-history--sort-predicate (x y) + "Sorting predicate which compares X and Y." + (or (< (cdr x) (cdr y)) + (and (= (cdr x) (cdr y)) + (corfu--length-string< (car x) (car y))))) + +(defun corfu-history--sort (cands) + "Sort CANDS by history." + (unless corfu-history--hash + (let ((ht (make-hash-table :test #'equal :size (length corfu-history))) + (decay (/ -1.0 (* corfu-history-duplicate corfu-history-decay)))) + (cl-loop for elem in corfu-history for idx from 0 + for r = (if-let* ((r (gethash elem ht))) + ;; Reduce duplicate rank with exponential decay. + (- r (round (* corfu-history-duplicate (exp (* decay idx))))) + ;; Never outrank the most recent element. + (if (= idx 0) (/ most-negative-fixnum 2) idx)) + do (puthash elem r ht)) + (setq corfu-history--hash ht))) + (cl-loop for ht = corfu-history--hash for max = most-positive-fixnum + for cand on cands do + (setcar cand (cons (car cand) (gethash (car cand) ht max)))) + (setq cands (sort cands #'corfu-history--sort-predicate)) + (cl-loop for cand on cands do (setcar cand (caar cand))) + cands) + +;;;###autoload +(define-minor-mode corfu-history-mode + "Update Corfu history and sort completions by history." + :global t :group 'corfu + (if corfu-history-mode + (add-function :override corfu-sort-function #'corfu-history--sort) + (remove-function corfu-sort-function #'corfu-history--sort))) + +(cl-defmethod corfu--insert :before (_status &context (corfu-history-mode (eql t))) + (when (>= corfu--index 0) + (unless (or (not (bound-and-true-p savehist-mode)) + (memq 'corfu-history (bound-and-true-p savehist-ignored-variables))) + (defvar savehist-minibuffer-history-variables) + (add-to-list 'savehist-minibuffer-history-variables 'corfu-history)) + (add-to-history 'corfu-history + (substring-no-properties + (nth corfu--index corfu--candidates))) + (setq corfu-history--hash nil))) + +(provide 'corfu-history) +;;; corfu-history.el ends here diff --git a/.config/emacs/lisp/minadstack/corfu.el b/.config/emacs/lisp/minadstack/corfu.el new file mode 100644 index 0000000..bd3b314 --- /dev/null +++ b/.config/emacs/lisp/minadstack/corfu.el @@ -0,0 +1,1444 @@ +;;; corfu.el --- COmpletion in Region FUnction -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2026 Free Software Foundation, Inc. + +;; Author: Daniel Mendler <mail@daniel-mendler.de> +;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> +;; Created: 2021 +;; Version: 2.10 +;; Package-Requires: ((emacs "29.1") (compat "31")) +;; URL: https://github.com/minad/corfu +;; Keywords: abbrev, convenience, matching, completion, text + +;; This file is part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Corfu enhances in-buffer completion with a small completion popup. +;; The current candidates are shown in a popup below or above the +;; point. The candidates can be selected by moving up and down. +;; Corfu is the minimalistic in-buffer completion counterpart of the +;; Vertico minibuffer UI. + +;;; Code: + +(require 'compat) +(eval-when-compile + (require 'cl-lib) + (require 'subr-x)) + +(defgroup corfu nil + "COmpletion in Region FUnction." + :link '(info-link :tag "Info Manual" "(corfu)") + :link '(url-link :tag "Website" "https://github.com/minad/corfu") + :link '(url-link :tag "Wiki" "https://github.com/minad/corfu/wiki") + :link '(emacs-library-link :tag "Library Source" "corfu.el") + :group 'convenience + :group 'tools + :group 'matching + :prefix "corfu-") + +(defcustom corfu-count 10 + "Maximal number of candidates to show." + :type 'natnum) + +(defcustom corfu-scroll-margin 2 + "Number of lines at the top and bottom when scrolling. +The value should lie between 0 and corfu-count/2." + :type 'natnum) + +(defcustom corfu-min-width 15 + "Popup minimum width in characters." + :type 'natnum) + +(defcustom corfu-max-width 100 + "Popup maximum width in characters." + :type 'natnum) + +(defcustom corfu-cycle nil + "Enable cycling for `corfu-next' and `corfu-previous'." + :type 'boolean) + +(defcustom corfu-on-exact-match nil + "Configure how a single exact match should be handled. +- nil: No special handling, continue completion. +- insert: Insert candidate, quit and call the `:exit-function'. +- quit: Quit completion without further action. +- show: Initiate completion even for a single match only." + :type '(choice (const insert) (const show) (const quit) (const nil))) + +(defcustom corfu-continue-commands + '(ignore universal-argument universal-argument-more digit-argument + "\\`corfu-" "\\`scroll-other-window") + "Continue Corfu completion after executing these commands. +The list can contain either command symbols or regular expressions." + :type '(repeat (choice regexp symbol))) + +(defcustom corfu-preview-current 'insert + "Preview currently selected candidate. +If the variable has the value `insert', the candidate is automatically +inserted on further input." + :type '(choice boolean (const insert))) + +(defcustom corfu-preselect 'valid + "Configure if the prompt or first candidate is preselected. +- prompt: Always select the prompt. +- first: Always select the first candidate. +- valid: Only select the prompt if valid and not equal to the first candidate. +- directory: Like first, but select the prompt if it is a directory." + :type '(choice (const prompt) (const valid) (const first) (const directory))) + +(defcustom corfu-separator ?\s + "Component separator character. +The character used for separating components in the input. The presence +of this separator character will inhibit quitting at completion +boundaries, so that any further characters can be entered. To enter the +first separator character, call `corfu-insert-separator' (bound to M-SPC +by default). Useful for multi-component completion styles such as +Orderless." + :type 'character) + +(defcustom corfu-quit-at-boundary 'separator + "Automatically quit at completion boundary. +nil: Never quit at completion boundary. +t: Always quit at completion boundary. +separator: Quit at boundary if no `corfu-separator' has been inserted." + :type '(choice boolean (const separator))) + +(defcustom corfu-quit-no-match 'separator + "Automatically quit if no matching candidate is found. +When staying alive even if there is no match a warning message is +shown in the popup. +nil: Stay alive even if there is no match. +t: Quit if there is no match. +separator: Only stay alive if there is no match and +`corfu-separator' has been inserted." + :type '(choice boolean (const separator))) + +(defcustom corfu-left-margin-width 0.5 + "Width of the left margin in units of the character width." + :type 'float) + +(defcustom corfu-right-margin-width 0.5 + "Width of the right margin in units of the character width." + :type 'float) + +(defcustom corfu-bar-width 0.2 + "Width of the bar in units of the character width." + :type 'float) + +(defcustom corfu-border-width 1 + "Width of the border in pixels, only applies to GUI Emacs." + :type 'natnum) + +(defcustom corfu-margin-formatters nil + "Registry for margin formatter functions. +Each function of the list is called with the completion metadata as +argument until an appropriate formatter is found. The function should +return a formatter function, which takes the candidate string and must +return a string, possibly an icon. In order to preserve correct popup +alignment, the length and display width of the returned string must +precisely span the same number of characters of the fixed-width popup +font. For example the kind-icon package returns a string of length 3 +with a display width of 3 characters." + :type 'hook) + +(defcustom corfu-sort-function #'corfu-sort-length-alpha + "Default sorting function. +This function is used if the completion table does not specify a +`display-sort-function'." + :type `(choice + (const :tag "No sorting" nil) + (const :tag "By length and alpha" ,#'corfu-sort-length-alpha) + (function :tag "Custom function"))) + +(defcustom corfu-sort-override-function nil + "Override sort function which overrides the `display-sort-function'. +This function is used even if a completion table specifies its +own sort function." + :type '(choice (const nil) function)) + +(defcustom corfu-auto nil + "Enable auto completion. +Auto completion is disabled by default for safety and unobtrusiveness. +Note that auto completion is particularly dangerous in untrusted files +since some completion functions may perform arbitrary code execution, +notably the Emacs built-in `elisp-completion-at-point'. See also the +settings `corfu-auto-delay', `corfu-auto-prefix' and +`corfu-auto-commands'." + :type 'boolean) + +(defgroup corfu-faces nil + "Faces used by Corfu." + :group 'corfu + :group 'faces) + +(defface corfu-default + '((((class color) (min-colors 88) (background dark)) :background "#191a1b") + (((class color) (min-colors 88) (background light)) :background "#f0f0f0") + (((background dark)) :background "gray" :foreground "black") + (t :background "gray")) + "Default face, foreground and background colors used for the popup.") + +(defface corfu-current + '((((class color) (min-colors 88) (background dark)) + :background "#00415e" :foreground "white" :extend t) + (((class color) (min-colors 88) (background light)) + :background "#c0efff" :foreground "black" :extend t) + (t :background "magenta" :foreground "white" :extend t)) + "Face used to highlight the currently selected candidate.") + +(defface corfu-bar + '((((class color) (min-colors 88) (background dark)) :background "#a8a8a8") + (((class color) (min-colors 88) (background light)) :background "#505050") + (t :background "black")) + "The background color is used for the scrollbar indicator.") + +(defface corfu-border + '((((class color) (min-colors 88) (background dark)) :background "#323232") + (((class color) (min-colors 88) (background light)) :background "#d7d7d7") + (t :background "gray")) + "The background color used for the thin border.") + +(defface corfu-annotations + '((t :inherit completions-annotations)) + "Face used for annotations.") + +(defface corfu-deprecated + '((t :inherit shadow :strike-through t)) + "Face used for deprecated candidates.") + +(defvar-keymap corfu-mode-map + :doc "Keymap used when `corfu-mode' is active.") + +(defvar-keymap corfu-map + :doc "Keymap used when popup is shown." + "<remap> <move-beginning-of-line>" #'corfu-prompt-beginning + "<remap> <move-end-of-line>" #'corfu-prompt-end + "<remap> <beginning-of-buffer>" #'corfu-first + "<remap> <end-of-buffer>" #'corfu-last + "<remap> <scroll-down-command>" #'corfu-scroll-down + "<remap> <scroll-up-command>" #'corfu-scroll-up + "<remap> <next-line>" #'corfu-next + "<remap> <previous-line>" #'corfu-previous + "<remap> <completion-at-point>" #'corfu-complete + "<remap> <keyboard-escape-quit>" #'corfu-reset + "<down>" #'corfu-next + "<up>" #'corfu-previous + "M-n" #'corfu-next + "M-p" #'corfu-previous + "C-g" #'corfu-quit + "RET" #'corfu-insert + "TAB" #'corfu-complete + "M-TAB" #'corfu-expand + "M-g" 'corfu-info-location + "M-h" 'corfu-info-documentation + "M-SPC" #'corfu-insert-separator) + +(defvar corfu--candidates nil + "List of candidates.") + +(defvar corfu--metadata nil + "Completion metadata.") + +(defvar corfu--base "" + "Base string, which is concatenated with the candidate.") + +(defvar corfu--total 0 + "Length of the candidate list `corfu--candidates'.") + +(defvar corfu--hilit #'identity + "Lazy candidate highlighting function.") + +(defvar corfu--index -1 + "Index of current candidate or negative for prompt selection.") + +(defvar corfu--preselect -1 + "Index of preselected candidate, negative for prompt selection.") + +(defvar corfu--scroll 0 + "Scroll position.") + +(defvar corfu--input nil + "Cons of last prompt contents and point.") + +(defvar corfu--preview-ov nil + "Current candidate overlay.") + +(defvar corfu--change-group nil + "Undo change group.") + +(defvar corfu--frame nil + "Popup frame.") + +(defvar corfu--width 0 + "Popup width of current completion to reduce width fluctuations.") + +(defconst corfu--initial-state + (mapcar + (lambda (k) (cons k (symbol-value k))) + '(corfu--base + corfu--candidates + corfu--hilit + corfu--index + corfu--preselect + corfu--scroll + corfu--input + corfu--total + corfu--preview-ov + corfu--change-group + corfu--metadata + corfu--width)) + "Initial Corfu state.") + +(defvar corfu--frame-parameters + '((no-accept-focus . t) + (no-focus-on-map . t) + (min-width . t) + (min-height . t) + (border-width . 0) + (outer-border-width . 0) + (vertical-scroll-bars . nil) + (horizontal-scroll-bars . nil) + (menu-bar-lines . 0) + (tool-bar-lines . 0) + (tab-bar-lines . 0) + (tab-bar-lines-keep-state . t) + (no-other-frame . t) + (unsplittable . t) + (undecorated . t) + (fullscreen . nil) + (cursor-type . nil) + (no-special-glyphs . t) + (desktop-dont-save . t) + (inhibit-double-buffering . t)) ;; Avoid display artifacts on X/Gtk builds + "Default child frame parameters. +It is recommended to avoid changing these parameters.") + +(defvar corfu--buffer-parameters + '((mode-line-format . nil) + (header-line-format . nil) + (tab-line-format . nil) + (tab-bar-format . nil) + (frame-title-format . "") + (truncate-lines . t) + (cursor-in-non-selected-windows . nil) + (cursor-type . nil) + (show-trailing-whitespace . nil) + (display-line-numbers . nil) + (left-fringe-width . 0) + (right-fringe-width . 0) + (left-margin-width . 0) + (right-margin-width . 0) + (fringes-outside-margins . 0) + (fringe-indicator-alist (continuation) (truncation)) + (indicate-empty-lines . nil) + (indicate-buffer-boundaries . nil) + (buffer-read-only . t) + (pixel-scroll-precision-mode . nil)) + "Default child frame buffer parameters. +It is recommended to avoid changing these parameters.") + +(defvar corfu--mouse-ignore-map + (let ((map (define-keymap "<touchscreen-begin>" #'ignore))) + (dotimes (i 7) + (dolist (k '(mouse down-mouse drag-mouse double-mouse triple-mouse)) + (keymap-set map (format "<%s-%s>" k (1+ i)) #'ignore))) + map) + "Ignore all mouse clicks.") + +(defun corfu--replace (beg end str) + "Replace range between BEG and END with STR." + (unless (equal str (buffer-substring-no-properties beg end)) + (completion--replace beg end str))) + +(defun corfu--capf-wrapper (fun &optional prefix) + "Wrapper for `completion-at-point' FUN. +The wrapper determines if the Capf is applicable at the current +position, performs sanity checking on the returned result and computes +the initial completion state. PREFIX is the minimum prefix length." + (pcase (funcall fun) + (`(,beg ,end ,table . ,plist) + (and (integer-or-marker-p beg) ;; Valid Capf result + (<= beg (point) end) ;; Sanity checking + ;; Check minimal prefix length if given. + (or (not prefix) + (let ((len (or (plist-get plist :company-prefix-length) + (- (point) beg)))) + (or (eq len t) (>= len prefix)))) + (let* ((str (buffer-substring-no-properties beg end)) + (pt (- (point) beg)) + (pred (plist-get plist :predicate)) + (state (corfu--compute (cons str pt) table pred))) + (cond ((alist-get 'corfu--candidates state) + `(,fun ,beg ,end ,table :corfu--state ,state ,@plist)) + ;; Stop with empty result for exclusive Capf. + ((not (eq 'no (plist-get plist :exclusive))) + '(nil)))))))) + +(defun corfu--make-buffer (name) + "Create buffer with NAME." + (let ((fr face-remapping-alist) + (ls line-spacing) + (buffer (get-buffer-create name))) + (with-current-buffer buffer + ;;; XXX HACK install mouse ignore map + (use-local-map corfu--mouse-ignore-map) + (dolist (var corfu--buffer-parameters) + (set-local (car var) (cdr var))) + (setq-local face-remapping-alist (copy-tree fr) + line-spacing ls) + (cl-pushnew 'corfu-default (alist-get 'default face-remapping-alist)) + buffer))) + +(defvar corfu--gtk-resize-child-frames + (let ((case-fold-search t)) + ;; XXX HACK to fix resizing on gtk3/gnome taken from posframe.el + ;; More information: + ;; * https://github.com/minad/corfu/issues/17 + ;; * https://gitlab.gnome.org/GNOME/mutter/-/issues/840 + ;; * https://lists.gnu.org/archive/html/emacs-devel/2020-02/msg00001.html + (and (string-match-p "gtk3" system-configuration-features) + (string-match-p "gnome\\|cinnamon" + (or (getenv "XDG_CURRENT_DESKTOP") + (getenv "DESKTOP_SESSION") "")) + 'resize-mode))) + +;; Not present on non-gtk/non-x builds +(defvar x-gtk-resize-child-frames) +(defvar x-fast-protocol-requests) + +;; Function adapted from posframe.el by tumashu +(defun corfu--make-frame (frame x y width height) + "Show current buffer in child frame at X/Y with WIDTH/HEIGHT. +FRAME is the existing frame." + (when-let* (((frame-live-p frame)) + (timer (frame-parameter frame 'corfu--hide-timer))) + (cancel-timer timer) + (set-frame-parameter frame 'corfu--hide-timer nil)) + (let* ((window-min-height 1) + (window-min-width 1) + (inhibit-redisplay t) + (x-fast-protocol-requests t) + (x-gtk-resize-child-frames corfu--gtk-resize-child-frames) + (before-make-frame-hook) + (after-make-frame-functions) + (parent (window-frame)) + (graphic (display-graphic-p parent)) + (params `((background-color + . ,(face-attribute 'corfu-default :background nil 'default)) + (font . ,(frame-parameter parent 'font)) + (right-fringe . ,right-fringe-width) + (left-fringe . ,left-fringe-width) + (internal-border-width . ,corfu-border-width) + (child-frame-border-width . ,corfu-border-width) + ,@corfu--frame-parameters))) + (unless (and (frame-live-p frame) + (eq (frame-parent frame) + (and (not (and graphic (bound-and-true-p exwm--connection))) + parent)) + ;; Handle mixed tty/graphical sessions + (eq graphic (display-graphic-p frame)) + ;; If there is more than one window, `frame-root-window' may + ;; return nil. Recreate the frame in this case. + (window-live-p (frame-root-window frame))) + (when frame (delete-frame frame)) + (setq frame (make-frame + `((name . ,(if graphic "EmacsCorfuGUI" "EmacsCorfuTTY")) + (parent-frame . ,parent) + (minibuffer . ,(minibuffer-window parent)) + (width . 0) (height . 0) (visibility . nil) + ,@params)))) + ;; XXX HACK Setting the same frame-parameter/face-background is not a nop. + ;; Check before applying the setting. Without the check, the frame flickers + ;; on Mac. We have to apply the face background before adjusting the frame + ;; parameter, otherwise the border is not updated. + (let ((new (face-attribute 'corfu-border :background nil 'default))) + (unless (equal (face-attribute 'internal-border :background frame 'default) new) + (set-face-background 'internal-border new frame)) + ;; XXX The Emacs Mac Port does not support `internal-border', we also have + ;; to set `child-frame-border'. + (unless (equal (face-attribute 'child-frame-border :background frame 'default) new) + (set-face-background 'child-frame-border new frame))) + ;; Reset frame parameters if they changed. For example `tool-bar-mode' + ;; overrides the parameter `tool-bar-lines' for every frame, including child + ;; frames. The child frame API is a pleasure to work with. It is full of + ;; lovely surprises. + (let* ((win (frame-root-window frame)) + (is (frame-parameters frame)) + (diff (cl-loop for p in params for (k . v) = p + unless (equal (alist-get k is) v) collect p))) + (when diff (modify-frame-parameters frame diff)) + ;; XXX HACK: `set-window-buffer' must be called to force fringe update. + (when (or diff (not (eq (window-buffer win) (current-buffer)))) + (set-window-buffer win (current-buffer))) + ;; Disallow selection of root window (gh:minad/corfu#63) + (set-window-parameter win 'no-delete-other-windows t) + (set-window-parameter win 'no-other-window t) + ;; Mark window as dedicated to prevent frame reuse (gh:minad/corfu#60) + (set-window-dedicated-p win t)) + (redirect-frame-focus frame parent) + (pcase-let* ((`(,ox ,oy ,right ,bottom) (frame-edges frame 'outer-edges)) + (border (* 2 corfu-border-width)) + (ow (- (- right ox) left-fringe-width right-fringe-width border)) + (oh (- (- bottom oy) border)) + (pos-change (or (/= x ox) (/= y oy))) + (size-change (or (/= ow width) (/= oh height)))) + (cond + ((and pos-change size-change) + ;; TODO: New Emacs 31 function for faster resizing/movement in one go. + ;; Add this function to Compat 31 as backport. + (static-if (fboundp 'set-frame-size-and-position-pixelwise) + (set-frame-size-and-position-pixelwise frame width height x y) + (set-frame-size frame width height t) + (set-frame-position frame x y))) + (pos-change (set-frame-position frame x y)) + (size-change (set-frame-size frame width height t))))) + (make-frame-visible frame) + ;; Unparent child frame if EXWM is used, otherwise EXWM buffers are drawn on + ;; top of the Corfu child frame. + (when (and (bound-and-true-p exwm--connection) + (display-graphic-p frame) (frame-parent frame)) + (redisplay t) + (set-frame-parameter frame 'parent-frame nil)) + frame) + +(defun corfu--hide-frame-deferred (frame) + "Deferred hiding of child FRAME." + (when (and (frame-live-p frame) (frame-visible-p frame)) + (set-frame-parameter frame 'corfu--hide-timer nil) + (make-frame-invisible frame) + (with-current-buffer (window-buffer (frame-root-window frame)) + (with-silent-modifications + (delete-region (point-min) (point-max)))))) + +(defun corfu--hide-frame (frame) + "Hide child FRAME." + (when (and (frame-live-p frame) (frame-visible-p frame)) + (cond + ((not (display-graphic-p frame)) + (corfu--hide-frame-deferred frame)) + ((not (frame-parameter frame 'corfu--hide-timer)) + (set-frame-parameter + frame 'corfu--hide-timer + (run-at-time 0 nil #'corfu--hide-frame-deferred frame)))))) + +(defun corfu--move-to-front (elem list) + "Move all ELEM (also duplicates) to front of LIST." + (if (member elem list) + (nconc (cl-loop for x in list if (equal x elem) collect x) + (delete elem list)) + list)) + +(defun corfu--filter-completions (&rest args) + "Compute all completions for ARGS with lazy highlighting." + (dlet ((completion-lazy-hilit t) (completion-lazy-hilit-fn nil)) + (static-if (>= emacs-major-version 30) + (cons (apply #'completion-all-completions args) completion-lazy-hilit-fn) + (cl-letf* ((orig-pcm (symbol-function #'completion-pcm--hilit-commonality)) + (orig-flex (symbol-function #'completion-flex-all-completions)) + ((symbol-function #'completion-flex-all-completions) + (lambda (&rest args) + ;; Unfortunately for flex we have to undo the lazy highlighting, since flex uses + ;; the completion-score for sorting, which is applied during highlighting. + (cl-letf (((symbol-function #'completion-pcm--hilit-commonality) orig-pcm)) + (apply orig-flex args)))) + ((symbol-function #'completion-pcm--hilit-commonality) + (lambda (pattern cands) + (setq completion-lazy-hilit-fn + (lambda (x) + ;; `completion-pcm--hilit-commonality' sometimes throws an internal error + ;; for example when entering "/sudo:://u". + (condition-case nil + (car (completion-pcm--hilit-commonality pattern (list x))) + (t x)))) + cands)) + ((symbol-function #'completion-hilit-commonality) + (lambda (cands prefix &optional base) + (setq completion-lazy-hilit-fn + (lambda (x) (car (completion-hilit-commonality (list x) prefix base)))) + (and cands (nconc cands base))))) + (cons (apply #'completion-all-completions args) completion-lazy-hilit-fn))))) + +(defun corfu--try-completion (str table pred pt &optional md) + "Complete STR given TABLE, predicate PRED, point PT and optional metadata MD." + (setq md (or md (completion-metadata (substring str 0 pt) table pred))) + (completion-try-completion str table pred pt md)) + +(defsubst corfu--length-string< (x y) + "Sorting predicate which compares X and Y first by length then by `string<'." + (or (< (length x) (length y)) (and (= (length x) (length y)) (string< x y)))) + +(defmacro corfu--partition! (list form) + "Evaluate FORM for every element and partition LIST." + (cl-with-gensyms (head1 head2 tail1 tail2) + `(let* ((,head1 (cons nil nil)) + (,head2 (cons nil nil)) + (,tail1 ,head1) + (,tail2 ,head2)) + (while ,list + (if (let ((it (car ,list))) ,form) + (progn + (setcdr ,tail1 ,list) + (pop ,tail1)) + (setcdr ,tail2 ,list) + (pop ,tail2)) + (pop ,list)) + (setcdr ,tail1 (cdr ,head2)) + (setcdr ,tail2 nil) + (setq ,list (cdr ,head1))))) + +(defun corfu--move-prefix-candidates-to-front (field cands) + "Move CANDS which match prefix of FIELD to the beginning." + (let* ((word (substring field 0 + (seq-position field corfu-separator))) + (len (length word))) + (corfu--partition! + cands + (and (>= (length it) len) + (eq t (compare-strings word 0 len it 0 len + completion-ignore-case)))))) + +(defun corfu--delete-dups (list) + "Delete `equal-including-properties' consecutive duplicates from LIST." + (let ((beg list)) + (while (cdr beg) + (let ((end (cdr beg))) + (while (equal (car beg) (car end)) (pop end)) + ;; The deduplication is quadratic in the number of duplicates. We could + ;; avoid this via a hash table taking properties into account. + (while (not (eq beg end)) + (let ((dup beg)) + (while (not (eq (cdr dup) end)) + (if (equal-including-properties (car beg) (cadr dup)) + (setcdr dup (cddr dup)) + (pop dup)))) + (pop beg))))) + list) + +(defun corfu--sort-function () + "Return the sorting function." + (or corfu-sort-override-function + (corfu--metadata-get 'display-sort-function) + corfu-sort-function)) + +(defun corfu--compute (input table pred) + "Compute state from INPUT, TABLE and PRED." + (pcase-let* ((`(,str . ,pt) input) + (before (substring str 0 pt)) + (after (substring str pt)) + (corfu--metadata (completion-metadata before table pred)) + ;; bug#47678: `completion-boundaries' fails for `partial-completion' + ;; if the cursor is moved before the slashes of "~//". + ;; See also vertico.el which has the same issue. + (bounds (condition-case nil + (completion-boundaries before table pred after) + (t (cons 0 (length after))))) + (field (substring str (car bounds) (+ pt (cdr bounds)))) + (completing-file (eq (corfu--metadata-get 'category) 'file)) + (`(,all . ,hl) (corfu--filter-completions str table pred pt corfu--metadata)) + (base (or (when-let* ((z (last all))) (prog1 (cdr z) (setcdr z nil))) 0)) + (corfu--base (substring str 0 base)) + (pre nil)) + ;; Filter the ignored file extensions. We cannot use modified predicate for + ;; this filtering, since this breaks the special casing in the + ;; `completion-file-name-table' for `file-exists-p' and `file-directory-p'. + (when completing-file (setq all (completion-pcm--filename-try-filter all))) + ;; Sort using the `display-sort-function' or the Corfu sort functions, and + ;; delete duplicates with respect to `equal-including-properties'. This is + ;; a deviation from the Vertico completion UI with more aggressive + ;; deduplication, where candidates are compared with `equal'. Corfu + ;; preserves candidates which differ in their text properties. Corfu tries + ;; to preserve text properties as much as possible, when calling the + ;; `:exit-function' to help Capfs with candidate disambiguation. This + ;; matters in particular for Lsp backends, which produce duplicates for + ;; overloaded methods. + (setq all (funcall (or (corfu--sort-function) #'identity) all) + all (corfu--move-prefix-candidates-to-front field all)) + (when (and completing-file (not (string-suffix-p "/" field))) + (setq all (corfu--move-to-front (concat field "/") all))) + (setq all (corfu--delete-dups (corfu--move-to-front field all)) + pre (if (or (eq corfu-preselect 'prompt) (not all) + (and completing-file (eq corfu-preselect 'directory) + (= (length corfu--base) (length str)) + (test-completion str table pred)) + (and (eq corfu-preselect 'valid) + (not (equal field (car all))) + (not (and completing-file (equal (concat field "/") (car all)))) + (test-completion str table pred))) + -1 0)) + `((corfu--input . ,input) + (corfu--base . ,corfu--base) + (corfu--metadata . ,corfu--metadata) + (corfu--candidates . ,all) + (corfu--total . ,(length all)) + (corfu--hilit . ,(or hl #'identity)) + (corfu--preselect . ,pre) + (corfu--index . ,(or (and (>= corfu--index 0) (/= corfu--index corfu--preselect) + (seq-position all (nth corfu--index corfu--candidates))) + pre))))) + +(defun corfu--update (&optional interruptible) + "Update state, optionally INTERRUPTIBLE." + (pcase-let* ((`(,beg ,end ,table ,pred . ,_) completion-in-region--data) + (pt (- (point) beg)) + (str (buffer-substring-no-properties beg end)) + (input (cons str pt))) + (unless (equal corfu--input input) + ;; Redisplay such that the input is immediately shown before the expensive + ;; candidate recomputation (gh:minad/corfu#48). See also corresponding + ;; issue gh:minad/vertico#89. + (when interruptible (redisplay)) + ;; Bind non-essential=t to prevent Tramp from opening new connections, + ;; without the user explicitly requesting it via M-TAB. + (pcase (let ((non-essential t)) + (if interruptible + (while-no-input (corfu--compute input table pred)) + (corfu--compute input table pred))) + ('nil (keyboard-quit)) + ((and state (pred consp)) + (dolist (s state) (set (car s) (cdr s)))))) + input)) + +(defun corfu--match-symbol-p (pattern sym) + "Return non-nil if SYM is matching an element of the PATTERN list." + (cl-loop with case-fold-search = nil + for x in (and (symbolp sym) pattern) + thereis (if (symbolp x) + (eq sym x) + (string-match-p x (symbol-name sym))))) + +(defun corfu--metadata-get (prop) + "Return PROP from completion metadata." + ;; Marginalia and various icon packages advise `completion-metadata-get' to + ;; inject their annotations, but are meant only for minibuffer completion. + ;; Therefore call `completion-metadata-get' without advices here. + (let ((completion-extra-properties (nth 4 completion-in-region--data))) + (funcall (advice--cd*r (symbol-function (compat-function completion-metadata-get))) + corfu--metadata prop))) + +(defun corfu--format-candidates (cands) + "Format annotated CANDS." + (cl-loop for c in cands do + (cl-loop for s in-ref c do + (setf s (replace-regexp-in-string "[ \t]*\n[ \t]*" " " s)))) + (let* ((cw (cl-loop for x in cands maximize (string-width (car x)))) + (pw (cl-loop for x in cands maximize (string-width (cadr x)))) + (sw (cl-loop for x in cands maximize (string-width (caddr x)))) + (width (min (max corfu--width corfu-min-width (+ pw cw sw)) + ;; -4 because of margins and some additional safety + corfu-max-width (- (frame-width) 4))) + (trunc (not (display-graphic-p)))) + (setq corfu--width width) + (list pw width + (cl-loop + for (cand prefix suffix) in cands collect + (let ((s (concat + prefix (make-string (- pw (string-width prefix)) ?\s) cand + (when (> sw 0) + (make-string (max 0 (- width pw (string-width cand) + (string-width suffix))) + ?\s)) + suffix))) + (if trunc (truncate-string-to-width s width) s)))))) + +(defun corfu--compute-scroll () + "Compute new scroll position." + (let ((off (max (min corfu-scroll-margin (/ corfu-count 2)) 0)) + (corr (if (= corfu-scroll-margin (/ corfu-count 2)) (1- (mod corfu-count 2)) 0))) + (setq corfu--scroll (min (max 0 (- corfu--total corfu-count)) + (max 0 (+ corfu--index off 1 (- corfu-count)) + (min (- corfu--index off corr) corfu--scroll)))))) + +(defun corfu--candidates-popup (pos) + "Show candidates popup at POS." + (corfu--compute-scroll) + (pcase-let* ((last (min (+ corfu--scroll corfu-count) corfu--total)) + (bar (ceiling (* corfu-count corfu-count) corfu--total)) + (lo (min (- corfu-count bar 1) (floor (* corfu-count corfu--scroll) corfu--total))) + (`(,mf . ,acands) + (corfu--affixate + (cl-loop + repeat corfu-count for c in (nthcdr corfu--scroll corfu--candidates) + collect (funcall corfu--hilit + ;; bug#77754: Highlight unquoted string. + (substring (or (get-text-property + 0 'completion--unquoted c) c)))))) + (`(,pw ,width ,fcands) (corfu--format-candidates acands)) + ;; Disable the left margin if a margin formatter is active. + (corfu-left-margin-width (if mf 0 corfu-left-margin-width))) + ;; Nonlinearity at the end and the beginning + (when (/= corfu--scroll 0) + (setq lo (max 1 lo))) + (when (/= last corfu--total) + (setq lo (min (- corfu-count bar 2) lo))) + (corfu--popup-show pos pw width fcands (- corfu--index corfu--scroll) + (and (> corfu--total corfu-count) lo) bar))) + +(defun corfu--range-valid-p () + "Check the completion range, return non-nil if valid." + (pcase-let ((buf (current-buffer)) + (pt (point)) + (`(,beg ,end . ,_) completion-in-region--data)) + (and beg end + (eq buf (marker-buffer end)) (eq buf (window-buffer)) + (<= beg pt end) + (save-excursion (goto-char beg) (<= (pos-bol) pt (pos-eol)))))) + +(defun corfu--continue-p () + "Check if completion should continue after a command. +Corfu bails out if the current buffer changed unexpectedly or if +point moved out of range, see `corfu--range-valid-p'. Also the +input must satisfy the `completion-in-region-mode--predicate' and +the last command must be listed in `corfu-continue-commands'." + (and (corfu--range-valid-p) + ;; We keep Corfu alive if a `overriding-terminal-local-map' is + ;; installed, e.g., the `universal-argument-map'. It would be good to + ;; think about a better criterion instead. Unfortunately relying on + ;; `this-command' alone is insufficient, since the value of + ;; `this-command' gets clobbered in the case of transient keymaps. + (or overriding-terminal-local-map + ;; Check if it is an explicitly listed continue command + (corfu--match-symbol-p corfu-continue-commands this-command) + (pcase-let ((`(,beg ,end . ,_) completion-in-region--data)) + (and (or (equal (or (car corfu--input) "") "") (< beg end)) ;; Check for empty input + (or (not corfu-quit-at-boundary) ;; Check separator or predicate + (and (eq corfu-quit-at-boundary 'separator) + (or (eq this-command #'corfu-insert-separator) + ;; with separator, any further chars allowed + (seq-contains-p (car corfu--input) corfu-separator))) + (funcall completion-in-region-mode--predicate))))))) + +(defun corfu--preview-current-p () + "Return t if the selected candidate is previewed." + (and corfu-preview-current (>= corfu--index 0) (/= corfu--index corfu--preselect))) + +(defun corfu--preview-current (beg end) + "Show current candidate as overlay given BEG and END." + (when (corfu--preview-current-p) + (corfu--preview-delete) + (setq beg (+ beg (length corfu--base)) + corfu--preview-ov (make-overlay beg end nil)) + (overlay-put corfu--preview-ov 'priority 1000) + (overlay-put corfu--preview-ov 'window (selected-window)) + (overlay-put corfu--preview-ov (if (= beg end) 'after-string 'display) + (substring-no-properties (nth corfu--index corfu--candidates))))) + +(defun corfu--preview-delete () + "Delete the preview overlay." + (when corfu--preview-ov + (delete-overlay corfu--preview-ov) + (setq corfu--preview-ov nil))) + +(defun corfu--window-change (_) + "Window and buffer change hook which quits Corfu." + (unless (corfu--range-valid-p) + (corfu-quit))) + +(defun corfu--debug (&rest _) + "Debugger used by `corfu--protect'." + (let ((inhibit-message t)) + (require 'backtrace) + (declare-function backtrace-to-string "backtrace") + (message "Corfu detected an error:\n%s" (backtrace-to-string))) + (let (message-log-max) + (message "%s %s" + (propertize "Corfu detected an error:" 'face 'error) + (substitute-command-keys "Press \\[view-echo-area-messages] to see the stack trace"))) + nil) + +(defun corfu--protect (fun) + "Protect FUN such that errors are caught. +If an error occurs, the FUN is retried with `debug-on-error' enabled and +the stack trace is shown in the *Messages* buffer." + (static-if (fboundp 'handler-bind) ;; Available on Emacs 30 + (ignore-errors + (handler-bind ((error #'corfu--debug)) + (funcall fun))) + (when (or debug-on-error (condition-case nil + (progn (funcall fun) nil) + (error t))) + (let ((debug-on-error t) + (debugger #'corfu--debug)) + (condition-case nil + (funcall fun) + ((debug error) nil)))))) + +(defun corfu--post-command () + "Refresh Corfu after last command." + (corfu--protect + (lambda () + (if (corfu--continue-p) + (corfu--exhibit) + (corfu-quit))))) + +(defun corfu--goto (index) + "Go to candidate with INDEX." + (setq corfu--index (max corfu--preselect (min index (1- corfu--total))))) + +(defun corfu--exit-function (str status cands) + "Call the `:exit-function' with STR and STATUS. +Lookup STR in CANDS to restore text properties." + (when-let* ((exit (plist-get completion-extra-properties :exit-function))) + (funcall exit (or (car (member str cands)) str) status))) + +(defun corfu--done (str status cands) + "Exit completion and call the exit function with STR and STATUS. +Lookup STR in CANDS to restore text properties." + (let ((completion-extra-properties (nth 4 completion-in-region--data))) + ;; For successful completions, amalgamate undo operations, + ;; such that completion can be undone in a single step. + (undo-amalgamate-change-group corfu--change-group) + (corfu-quit) + (corfu--exit-function str status cands))) + +(defun corfu--setup (beg end table pred) + "Setup Corfu completion state. +See `completion-in-region' for the arguments BEG, END, TABLE, PRED." + (let ((props completion-extra-properties)) + (when (eq (car props) :corfu--state) + (dolist (s (cadr props)) (set (car s) (cdr s))) + (setq props (cddr props))) + (setq end (if (and (markerp end) (marker-insertion-type end)) end (copy-marker end t)) + completion-in-region--data (list (+ 0 beg) end table pred props))) + (completion-in-region-mode) + (activate-change-group (setq corfu--change-group (prepare-change-group))) + (setcdr (assq #'completion-in-region-mode minor-mode-overriding-map-alist) corfu-map) + (add-hook 'pre-command-hook #'corfu--prepare nil 'local) + (add-hook 'window-selection-change-functions #'corfu--window-change nil 'local) + (add-hook 'window-buffer-change-functions #'corfu--window-change nil 'local) + (add-hook 'post-command-hook #'corfu--post-command) + ;; Disable default post-command handling, since we have our own + ;; checks in `corfu--post-command'. + (remove-hook 'post-command-hook #'completion-in-region--postch) + (let ((sym (make-symbol "corfu--teardown")) + (buf (current-buffer))) + (fset sym (lambda () + ;; Ensure that the tear-down runs in the correct buffer, if still alive. + (unless completion-in-region-mode + (remove-hook 'completion-in-region-mode-hook sym) + (corfu--teardown buf)))) + (add-hook 'completion-in-region-mode-hook sym))) + +(defun corfu--in-region (&rest args) + "Corfu completion in region function called with ARGS." + ;; XXX We can get an endless loop when `completion-in-region-function' is set + ;; globally to `corfu--in-region'. This should never happen. + (apply (if (corfu--popup-support-p) #'corfu--in-region-1 + (default-value 'completion-in-region-function)) + args)) + +(defun corfu--in-region-1 (beg end table pred) + "Complete in region, see `completion-in-region' for BEG, END, TABLE, PRED." + (barf-if-buffer-read-only) + ;; Restart the completion. This can happen for example if C-M-/ + ;; (`dabbrev-completion') is pressed while the Corfu popup is already open. + (when completion-in-region-mode (corfu-quit)) + (let* ((pt (max 0 (- (point) beg))) + (str (buffer-substring-no-properties beg end)) + (input (cons str pt)) + (md (completion-metadata (substring str 0 pt) table pred)) + (threshold (completion--cycle-threshold md)) + (completion-in-region-mode-predicate + (or completion-in-region-mode-predicate #'always))) + (pcase (corfu--try-completion str table pred pt md) + ('nil (corfu--message "No match") nil) + ('t (goto-char end) + (corfu--message "Sole match") + (if (eq corfu-on-exact-match 'show) + (corfu--setup beg end table pred) + (corfu--exit-function + str 'finished + (alist-get 'corfu--candidates (corfu--compute input table pred)))) + t) + ((and newinp `(,newstr . ,newpt)) + (setq end (copy-marker end t)) + (corfu--replace beg end newstr) + (goto-char (+ beg newpt)) + (let* ((state (corfu--compute newinp table pred)) + (base (alist-get 'corfu--base state)) + (total (alist-get 'corfu--total state)) + (cands (alist-get 'corfu--candidates state))) + (cond + ((= total 0) + (when (test-completion newstr table pred) + (corfu--exit-function newstr 'finished nil))) + ((= total 1) + ;; Setup popup if `corfu-on-exact-match' is `show' or if completion + ;; can continue. + (if (or (eq corfu-on-exact-match 'show) + (consp (corfu--try-completion newstr table pred newpt))) + (corfu--setup beg end table pred) + (corfu--exit-function (car cands) 'finished nil))) + ;; Too many candidates for cycling -> Setup popup. + ((or (not threshold) (and (not (eq threshold t)) (< threshold total))) + (corfu--setup beg end table pred)) + (t + ;; Cycle through candidates. + (corfu--cycle-candidates total cands (+ (length base) beg) end) + ;; Do not show Corfu when completion is finished after the candidate. + (unless (equal (completion-boundaries (car cands) table pred "") '(0 . 0)) + (corfu--setup beg end table pred))))) + t)))) + +(defun corfu--message (&rest msg) + "Show completion MSG." + (let (message-log-max) (apply #'message msg))) + +(defun corfu--cycle-candidates (total cands beg end) + "Cycle between TOTAL number of CANDS. +See `completion-in-region' for the arguments BEG, END, TABLE, PRED." + (let* ((idx 0) + (map (make-sparse-keymap)) + (replace (lambda () + (interactive) + (corfu--replace beg end (nth idx cands)) + (corfu--message "Cycling %d/%d..." (1+ idx) total) + (setq idx (mod (1+ idx) total)) + (set-transient-map map)))) + (define-key map [remap completion-at-point] replace) + (define-key map [remap corfu-complete] replace) + (define-key map (vector last-command-event) replace) + (funcall replace))) + +(cl-defgeneric corfu--popup-show (pos off width lines &optional curr lo bar) + "Show LINES as popup at POS - OFF. +WIDTH is the width of the popup. +The current candidate CURR is highlighted. +A scroll bar is displayed from LO to LO+BAR." + (let ((lh (max (default-line-height) (cdr (posn-object-width-height pos))))) + (with-current-buffer (corfu--make-buffer " *corfu*") + (let* ((ch (default-line-height)) + (cw (default-font-width)) + ;; bug#74214, bug#37755, bug#37689: Even for larger fringes, fringe + ;; bitmaps can only have a width between 1 and 16. Therefore we + ;; restrict the fringe width to 16 pixel. This restriction may + ;; cause problem on HDPi systems. Hopefully Emacs will adopt + ;; larger fringe bitmaps in the future and lift the restriction. + (ml (min 16 (ceiling (* cw corfu-left-margin-width)))) + (mr (min 16 (ceiling (* cw corfu-right-margin-width)))) + (bw (min mr (ceiling (* cw corfu-bar-width)))) + (graphic (display-graphic-p)) + (marginl (and (not graphic) (propertize " " 'display `(space :width (,ml))))) + (sbar (if graphic + #(" " 0 1 (display (right-fringe corfu--bar corfu--bar))) + (concat + (propertize " " 'display `(space :align-to (- right (,bw)))) + (propertize " " 'face 'corfu-bar 'display `(space :width (,bw)))))) + (cbar (if graphic + #(" " 0 1 (display (left-fringe corfu--nil corfu-current)) + 1 2 (display (right-fringe corfu--bar corfu--cbar))) + sbar)) + (cmargin (and graphic + #(" " 0 1 (display (left-fringe corfu--nil corfu-current)) + 1 2 (display (right-fringe corfu--nil corfu-current))))) + (pos (posn-x-y pos)) + (width (+ (* width cw) (if graphic 0 (+ ml mr)))) + ;; XXX HACK: Minimum popup height must be at least 1 line of the + ;; parent frame (gh:minad/corfu#261). + (height (max lh (* (length lines) ch))) + (edge (window-inside-pixel-edges)) + (border (if graphic corfu-border-width 0)) + (x (max 0 (min (+ (car edge) (- (or (car pos) 0) ml (* cw off) border)) + (- (frame-pixel-width) width + (if graphic (+ ml mr (* 2 border)) 0))))) + (yb (+ (cadr edge) (or (cdr pos) 0) lh + (static-if (< emacs-major-version 31) (window-tab-line-height) 0))) + (y (if (> (+ yb (* corfu-count ch) lh lh) (frame-pixel-height)) + (- yb height lh border border) + yb)) + (bmp (logxor (1- (ash 1 mr)) (1- (ash 1 bw))))) + (setq left-fringe-width (if graphic ml 0) right-fringe-width (if graphic mr 0)) + ;; Define an inverted corfu--bar face + (unless (equal (and (facep 'corfu--bar) (face-attribute 'corfu--bar :foreground)) + (face-attribute 'corfu-bar :background)) + (set-face-attribute (make-face 'corfu--bar) nil + :foreground (face-attribute 'corfu-bar :background))) + (unless (or (= right-fringe-width 0) (eq (get 'corfu--bar 'corfu--bmp) bmp)) + (put 'corfu--bar 'corfu--bmp bmp) + (define-fringe-bitmap 'corfu--bar (vector (lognot bmp)) 1 mr '(top periodic)) + (define-fringe-bitmap 'corfu--nil [0] 1 1) + ;; Fringe bitmaps require symbol face specification, define internal face. + (set-face-attribute (make-face 'corfu--cbar) nil + :inherit '(corfu--bar corfu-current))) + (with-silent-modifications + (delete-region (point-min) (point-max)) + (apply #'insert + (cl-loop for row from 0 for line in lines collect + (let ((str (concat marginl line + (if (and lo (<= lo row (+ lo bar))) + (if (eq row curr) cbar sbar) + (and (eq row curr) cmargin)) + "\n"))) + (when (eq row curr) + (add-face-text-property + 0 (length str) 'corfu-current 'append str)) + str))) + (goto-char (point-min))) + (setq corfu--frame (corfu--make-frame corfu--frame x y width height)))))) + +(cl-defgeneric corfu--popup-hide () + "Hide Corfu popup." + (corfu--hide-frame corfu--frame)) + +(cl-defgeneric corfu--popup-support-p () + "Return non-nil if child frames are supported." + (or (display-graphic-p) (featurep 'tty-child-frames))) + +(cl-defgeneric corfu--insert (status) + "Insert current candidate, exit with STATUS if non-nil." + ;; XXX There is a small bug here, depending on interpretation. + ;; When completing "~/emacs/master/li|/calc" where "|" is the + ;; cursor, then the candidate only includes the prefix + ;; "~/emacs/master/lisp/", but not the suffix "/calc". Default + ;; completion has the same problem when selecting in the + ;; *Completions* buffer. See bug#48356. + (pcase-let* ((`(,beg ,end . ,_) completion-in-region--data) + (str (concat corfu--base (nth corfu--index corfu--candidates)))) + (corfu--replace beg end str) + (corfu--goto -1) ;; Reset selection, completion may continue. + (when status (corfu--done str status nil)) + str)) + +(cl-defgeneric corfu--affixate (cands) + "Annotate CANDS with annotation function." + (let* ((dep (corfu--metadata-get 'company-deprecated)) + (mf (let ((completion-extra-properties (nth 4 completion-in-region--data))) + (run-hook-with-args-until-success 'corfu-margin-formatters corfu--metadata)))) + (setq cands + (if-let* ((aff (corfu--metadata-get 'affixation-function))) + (funcall aff cands) + (if-let* ((ann (corfu--metadata-get 'annotation-function))) + (cl-loop for cand in cands collect + (let ((suff (or (funcall ann cand) ""))) + ;; The default completion UI adds the + ;; `completions-annotations' face if no other faces are + ;; present. We use a custom `corfu-annotations' face to + ;; allow further styling which fits better for popups. + (unless (text-property-not-all 0 (length suff) 'face nil suff) + (setq suff (propertize suff 'face 'corfu-annotations))) + (list cand "" suff))) + (cl-loop for cand in cands collect (list cand "" ""))))) + (cl-loop for x in cands for (c . _) = x do + (when mf + (setf (cadr x) (funcall mf c))) + (when (and dep (funcall dep c)) + (setcar x (setq c (substring c))) + (add-face-text-property 0 (length c) 'corfu-deprecated 'append c))) + (cons mf cands))) + +(cl-defgeneric corfu--prepare () + "Insert selected candidate unless command is marked to continue completion." + (corfu--preview-delete) + ;; Ensure that state is initialized before next Corfu command + (when (and (symbolp this-command) (string-prefix-p "corfu-" (symbol-name this-command))) + (corfu--update)) + ;; If the next command is not listed in `corfu-continue-commands', insert the + ;; currently selected candidate and bail out of completion. This way you can + ;; continue typing after selecting a candidate. The candidate will be inserted + ;; and your new input will be appended. + (and (corfu--preview-current-p) (eq corfu-preview-current 'insert) + ;; See the comment about `overriding-local-map' in `corfu--post-command'. + (not (or overriding-terminal-local-map + (corfu--match-symbol-p corfu-continue-commands this-command))) + (corfu--insert 'exact))) + +(cl-defgeneric corfu--exhibit () + "Exhibit Corfu UI." + (pcase-let ((`(,beg ,end ,table ,pred . ,_) completion-in-region--data) + (`(,str . ,pt) (corfu--update 'interruptible))) + (cond + ;; 1) Single exactly matching candidate and no further completion is possible. + ((and corfu-on-exact-match + (not (eq corfu-on-exact-match 'show)) + (equal corfu--candidates (list str)) + (not (consp (corfu--try-completion str table pred pt)))) + (if (eq corfu-on-exact-match 'quit) + (corfu-quit) + (corfu--done (car corfu--candidates) 'finished nil))) + ;; 2) There exist candidates => Show candidates popup. + (corfu--candidates + (let ((pos (posn-at-point (min (point-max) (+ beg (length corfu--base)))))) + (corfu--preview-current beg end) + (corfu--candidates-popup pos))) + ;; 3) No candidates & `corfu-quit-no-match' & initialized => Confirmation popup. + ((pcase-exhaustive corfu-quit-no-match + ('t nil) + ('nil corfu--input) + ('separator (seq-contains-p (car corfu--input) corfu-separator))) + (corfu--popup-show (posn-at-point beg) 0 8 '(#("No match" 0 8 (face italic))))) + ;; 4) No candidates & initialized => Quit. + (corfu--input (corfu-quit))))) + +(cl-defgeneric corfu--teardown (buffer) + "Tear-down Corfu in BUFFER, which might be dead at this point." + (corfu--popup-hide) + (corfu--preview-delete) + (remove-hook 'post-command-hook #'corfu--post-command) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (remove-hook 'window-selection-change-functions #'corfu--window-change 'local) + (remove-hook 'window-buffer-change-functions #'corfu--window-change 'local) + (remove-hook 'pre-command-hook #'corfu--prepare 'local) + (accept-change-group corfu--change-group))) + (cl-loop for (k . v) in corfu--initial-state do (set k v))) + +(defun corfu-sort-length-alpha (list) + "Sort LIST by length and alphabetically." + (sort list #'corfu--length-string<)) + +(defun corfu-quit () + "Quit Corfu completion." + (interactive) + (completion-in-region-mode -1)) + +(defun corfu-reset () + "Reset Corfu completion. +This command can be executed multiple times by hammering the ESC key. If a +candidate is selected, unselect the candidate. Otherwise reset the input. If +there hasn't been any input, then quit." + (interactive) + (if (/= corfu--index corfu--preselect) + (progn + (corfu--goto -1) + (setq this-command #'corfu-first)) + ;; Cancel all changes and start new change group. + (pcase-let* ((`(,beg ,end . ,_) completion-in-region--data) + (str (buffer-substring-no-properties beg end))) + (cancel-change-group corfu--change-group) + (goto-char end) + (activate-change-group (setq corfu--change-group (prepare-change-group))) + ;; Quit when resetting, when input did not change. + (when (equal str (buffer-substring-no-properties beg end)) + (corfu-quit))))) + +(defun corfu-insert-separator () + "Insert a separator character, inhibiting quit on completion boundary. +If the currently selected candidate is previewed, jump to the input +prompt instead. See `corfu-separator' for more details." + (interactive) + (if (not (corfu--preview-current-p)) + (insert corfu-separator) + (corfu--goto -1) + (unless (or (= (car completion-in-region--data) (point)) + (= (char-before) corfu-separator)) + (insert corfu-separator)))) + +(defun corfu-next (&optional n) + "Go forward N candidates." + (interactive "p") + (let ((index (+ corfu--index (or n 1)))) + (corfu--goto + (cond + ((not corfu-cycle) index) + ((= corfu--total 0) -1) + ((< corfu--preselect 0) (1- (mod (1+ index) (1+ corfu--total)))) + (t (mod index corfu--total)))))) + +(defun corfu-previous (&optional n) + "Go backward N candidates." + (interactive "p") + (corfu-next (- (or n 1)))) + +(defun corfu-scroll-down (&optional n) + "Go back by N pages." + (interactive "p") + (corfu--goto (max 0 (- corfu--index (* (or n 1) corfu-count))))) + +(defun corfu-scroll-up (&optional n) + "Go forward by N pages." + (interactive "p") + (corfu-scroll-down (- (or n 1)))) + +(defun corfu-first () + "Go to first candidate. +If the first candidate is already selected, go to the prompt." + (interactive) + (corfu--goto (if (> corfu--index 0) 0 -1))) + +(defun corfu-last () + "Go to last candidate." + (interactive) + (corfu--goto (1- corfu--total))) + +(defun corfu-prompt-beginning (arg) + "Move to beginning of the prompt line. +If the point is already the beginning of the prompt move to the +beginning of the line. If ARG is not 1 or nil, move backward ARG - 1 +lines first." + (interactive "^p") + (let ((beg (car completion-in-region--data))) + (if (or (not (eq arg 1)) + (and (= corfu--preselect corfu--index) (= (point) beg))) + (move-beginning-of-line arg) + (corfu--goto -1) + (goto-char beg)))) + +(defun corfu-prompt-end (arg) + "Move to end of the prompt line. +If the point is already the end of the prompt move to the end of +the line. If ARG is not 1 or nil, move forward ARG - 1 lines +first." + (interactive "^p") + (let ((end (cadr completion-in-region--data))) + (if (or (not (eq arg 1)) + (and (= corfu--preselect corfu--index) (= (point) end))) + (move-end-of-line arg) + (corfu--goto -1) + (goto-char end)))) + +(defun corfu-complete () + "Complete current input. +If a candidate is selected, insert it. Otherwise invoke +`corfu-expand'. Return non-nil if the input has been expanded." + (interactive) + (if (< corfu--index 0) + (corfu-expand) + ;; Continue completion with selected candidate. Exit with status 'finished + ;; if input is a valid match and no further completion is possible. + (pcase-let ((`(,_beg ,_end ,table ,pred . ,_) completion-in-region--data) + (newstr (corfu--insert nil))) + (and (test-completion newstr table pred) + (or (not (consp (corfu--try-completion newstr table pred (length newstr)))) + ;; Additionally finish completion if at the end of a boundary, + ;; even if other longer candidates match, since the user invoked + ;; `corfu-complete' with an explicitly selected candidate! + (equal (completion-boundaries newstr table pred "") '(0 . 0))) + (corfu--done newstr 'finished nil)) + t))) + +(defun corfu-expand () + "Expands the common prefix of all candidates. +If the currently selected candidate is previewed, invoke +`corfu-complete' instead. Expansion relies on the completion +styles via `completion-try-completion'. Return non-nil if the +input has been expanded." + (interactive) + (if (corfu--preview-current-p) + (corfu-complete) + (pcase-let* ((`(,beg ,end ,table ,pred . ,_) completion-in-region--data) + (pt (max 0 (- (point) beg))) + (str (buffer-substring-no-properties beg end))) + (pcase (corfu--try-completion str table pred pt) + ('t + (goto-char end) + (corfu--done str 'finished corfu--candidates) + t) + ((and `(,newstr . ,newpt) (guard (not (and (= pt newpt) (equal newstr str))))) + (corfu--replace beg end newstr) + (goto-char (+ beg newpt)) + ;; Exit with status 'finished if input is a valid match + ;; and no further completion is possible. + (and (test-completion newstr table pred) + (not (consp (corfu--try-completion newstr table pred newpt))) + (corfu--done newstr 'finished corfu--candidates)) + t))))) + +(defun corfu-insert () + "Insert current candidate. +Quit if no candidate is selected." + (interactive) + (if (>= corfu--index 0) + (corfu--insert 'finished) + (corfu-quit))) + +(defun corfu-send () + "Insert current candidate and send it when inside comint or eshell." + (interactive) + (corfu-insert) + (cond + ((and (derived-mode-p 'eshell-mode) (fboundp 'eshell-send-input)) + (eshell-send-input)) + ((and (derived-mode-p 'comint-mode) (fboundp 'comint-send-input)) + (comint-send-input)))) + +;;;###autoload +(define-minor-mode corfu-mode + "COmpletion in Region FUnction." + :group 'corfu :keymap corfu-mode-map + (cond + (corfu-mode + (when corfu-auto + (require 'corfu-auto) + (add-hook 'post-command-hook 'corfu-auto--post-command 10 'local)) + (setq-local completion-in-region-function #'corfu--in-region)) + (t + (remove-hook 'post-command-hook 'corfu-auto--post-command 'local) + (kill-local-variable 'completion-in-region-function)))) + +(defcustom global-corfu-minibuffer t + "Corfu should be enabled in the minibuffer by `global-corfu-mode'. +The variable can either be t, nil or a custom predicate function. If +the variable is set to t, Corfu is only enabled if the minibuffer has +local `completion-at-point-functions'." + :type '(choice (const t) (const nil) function) + :group 'corfu) + +;;;###autoload +(define-globalized-minor-mode global-corfu-mode + corfu-mode corfu--on + :group 'corfu + :predicate t + (remove-hook 'minibuffer-setup-hook #'corfu--minibuffer-on) + (when (and global-corfu-mode global-corfu-minibuffer) + (add-hook 'minibuffer-setup-hook #'corfu--minibuffer-on 100))) + +(defun corfu--on () + "Enable `corfu-mode' in the current buffer respecting `global-corfu-modes'." + (unless (or noninteractive buffer-read-only (eq (aref (buffer-name) 0) ?\s)) + (corfu-mode))) + +(defun corfu--minibuffer-on () + "Enable `corfu-mode' in the minibuffer respecting `global-corfu-minibuffer'." + (when (and global-corfu-minibuffer (not noninteractive) + (if (functionp global-corfu-minibuffer) + (funcall global-corfu-minibuffer) + (local-variable-p 'completion-at-point-functions))) + (corfu-mode))) + +;; Do not show Corfu commands with M-X +(dolist (sym '( corfu-next corfu-previous corfu-first corfu-last corfu-quit corfu-reset + corfu-complete corfu-insert corfu-scroll-up corfu-scroll-down corfu-expand + corfu-send corfu-insert-separator corfu-prompt-beginning corfu-prompt-end + corfu-info-location corfu-info-documentation ;; autoloads in corfu-info.el + corfu-quick-jump corfu-quick-insert corfu-quick-complete)) ;; autoloads in corfu-quick.el + (put sym 'completion-predicate #'ignore)) + +(defun corfu--capf-wrapper-advice (orig fun which) + "Around advice for `completion--capf-wrapper'. +The ORIG function takes the FUN and WHICH arguments." + (if corfu-mode (corfu--capf-wrapper fun) (funcall orig fun which))) + +(defun corfu--eldoc-advice () + "Return non-nil if Corfu is currently not active." + (not (and corfu-mode completion-in-region-mode))) + +;; Install advice which fixes `completion--capf-wrapper', such that it respects +;; the completion styles for non-exclusive Capfs. See also the fixme comment in +;; the `completion--capf-wrapper' function in minibuffer.el. +(advice-add #'completion--capf-wrapper :around #'corfu--capf-wrapper-advice) + +;; Register Corfu with ElDoc +(advice-add #'eldoc-display-message-no-interference-p + :before-while #'corfu--eldoc-advice) +(eldoc-add-command #'corfu-complete #'corfu-insert #'corfu-expand #'corfu-send) + +(with-eval-after-load 'corfu-terminal + (when (featurep 'tty-child-frames) + (display-warning 'corfu "`corfu-terminal' is not needed on Emacs 31"))) + +(provide 'corfu) +;;; corfu.el ends here diff --git a/.config/emacs/lisp/minadstack/marginalia.el b/.config/emacs/lisp/minadstack/marginalia.el new file mode 100644 index 0000000..3c49eed --- /dev/null +++ b/.config/emacs/lisp/minadstack/marginalia.el @@ -0,0 +1,1461 @@ +;;; marginalia.el --- Enrich existing commands with completion annotations -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2026 Free Software Foundation, Inc. + +;; Author: Omar Antolín Camarena <omar@matem.unam.mx>, Daniel Mendler <mail@daniel-mendler.de> +;; Maintainer: Omar Antolín Camarena <omar@matem.unam.mx>, Daniel Mendler <mail@daniel-mendler.de> +;; Created: 2020 +;; Version: 2.10 +;; Package-Requires: ((emacs "29.1") (compat "30")) +;; URL: https://github.com/minad/marginalia +;; Keywords: docs, help, matching, completion + +;; This file is part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Enrich existing commands with completion annotations. The information +;; associated with the completion candidates is shown in the minibuffer or the +;; *Completions* buffer. For files the owner and permissions are shown, for +;; buffers the major mode and modification status and for functions or commands +;; the docstring is shown. + +;;; Code: + +(require 'compat) +(eval-when-compile + (require 'subr-x) + (require 'cl-lib)) + +;;;; Customization + +(defgroup marginalia nil + "Enrich existing commands with completion annotations." + :link '(info-link :tag "Info Manual" "(marginalia)") + :link '(url-link :tag "Website" "https://github.com/minad/marginalia") + :link '(emacs-library-link :tag "Library Source" "marginalia.el") + :group 'help + :group 'docs + :group 'minibuffer + :prefix "marginalia-") + +(defcustom marginalia-field-width 80 + "Maximum truncation width of annotation fields. + +This value is adjusted depending on the `window-width'." + :type 'natnum) + +(defcustom marginalia-separator " " + "Annotation field separator." + :type 'string) + +(defcustom marginalia-align 'left + "Alignment of the annotations." + :type '(choice (const :tag "Left" left) + (const :tag "Center" center) + (const :tag "Right" right))) + +(defcustom marginalia-align-offset 0 + "Additional offset added to the alignment." + :type 'natnum) + +(defcustom marginalia-max-relative-age (* 60 60 24 14) + "Maximum relative age in seconds displayed by the file annotator. + +Set to `most-positive-fixnum' to always use a relative age, or 0 to never show +a relative age." + :type 'natnum) + +(defcustom marginalia-remote-file-regexps + '("\\`/\\([^/|:]+\\):") ;; Tramp path + "List of remote file regexps where the files should not be annotated. + +The first match group is displayed instead of the detailed file +attribute information. For Tramp paths, the protocol is +displayed instead." + :type '(repeat regexp)) + +(defcustom marginalia-annotators + (mapcar + (lambda (x) (append x (list 'builtin 'none))) + `((command ,#'marginalia-annotate-command ,#'marginalia-annotate-binding) + (embark-keybinding ,#'marginalia-annotate-embark-keybinding) + (customize-group ,#'marginalia-annotate-customize-group) + (variable ,#'marginalia-annotate-variable) + (function ,#'marginalia-annotate-function) + (face ,#'marginalia-annotate-face) + (color ,#'marginalia-annotate-color) + (unicode-name ,#'marginalia-annotate-char) + (minor-mode ,#'marginalia-annotate-minor-mode) + (symbol ,#'marginalia-annotate-symbol) + (environment-variable ,#'marginalia-annotate-environment-variable) + (input-method ,#'marginalia-annotate-input-method) + (coding-system ,#'marginalia-annotate-coding-system) + (charset ,#'marginalia-annotate-charset) + (package ,#'marginalia-annotate-package) + (imenu ,#'marginalia-annotate-imenu) + (bookmark ,#'marginalia-annotate-bookmark) + (file ,#'marginalia-annotate-file) + (project-file ,#'marginalia-annotate-project-file) + (project-buffer ,#'marginalia-annotate-buffer) + (buffer ,#'marginalia-annotate-buffer) + (library ,#'marginalia-annotate-library) + (theme ,#'marginalia-annotate-theme) + (tab ,#'marginalia-annotate-tab) + (frame ,#'marginalia-annotate-frame) + (multi-category ,#'marginalia-annotate-multi-category))) + "Annotator function registry. +Associates completion categories with annotation functions. Each +annotation function must return a string, which is appended to the +completion candidate. The annotation functions are executed in the +original window and the original buffer, if still alive." + :type '(alist :key-type symbol :value-type (repeat symbol))) + +(defcustom marginalia-classifiers + (list #'marginalia-classify-by-command-name + #'marginalia-classify-original-category + #'marginalia-classify-by-prompt + #'marginalia-classify-symbol) + "List of functions to determine current completion category. +Each function should take no arguments and return a symbol +indicating the category, or nil to indicate it could not +determine it." + :type 'hook) + +(defcustom marginalia-prompt-categories + '(("\\<customize group\\>" . customize-group) + ("\\<M-x\\>" . command) + ("\\<package\\>" . package) + ("\\<bookmark\\>" . bookmark) + ("\\<color\\>" . color) + ("\\<face\\>" . face) + ("\\<environment variable\\>" . environment-variable) + ("\\<function\\|\\(?:hook\\|advice\\) to remove\\>" . function) + ("\\<variable\\>" . variable) + ("\\<input method\\>" . input-method) + ("\\<charset\\>" . charset) + ("\\<coding system\\>" . coding-system) + ("\\<minor mode\\>" . minor-mode) + ("\\<kill-ring\\>" . kill-ring) + ("\\<tab by name\\>" . tab) + ("\\<frame\\>" . frame) + ("\\<library\\>" . library) + ("\\<theme\\>" . theme)) + "Associates regexps to match against minibuffer prompts with categories. +The prompts are matched case-insensitively." + :type '(alist :key-type regexp :value-type symbol)) + +(defcustom marginalia-censor-variables + '("pass\\|auth-source-netrc-cache\\|auth-source-.*-nonce\\|api-?key") + "The value of variables matching any of these regular expressions is not shown. +This configuration variable is useful to hide variables which may +hold sensitive data, e.g., passwords. The variable names are +matched case-sensitively." + :type '(repeat (choice symbol regexp))) + +(defcustom marginalia-command-categories + `((,#'imenu . imenu) + (,#'recentf-open . file) + (,#'where-is . command)) + "Associate commands with a completion category. +The value of `this-command' is used as key for the lookup." + :type '(alist :key-type symbol :value-type symbol)) + +(defgroup marginalia-faces nil + "Faces used by `marginalia-mode'." + :group 'marginalia + :group 'faces) + +(defface marginalia-key + '((t :inherit font-lock-keyword-face)) + "Face used to highlight keys.") + +(defface marginalia-type + '((t :inherit marginalia-key)) + "Face used to highlight types.") + +(defface marginalia-char + '((t :inherit marginalia-key)) + "Face used to highlight character annotations.") + +(defface marginalia-lighter + '((t :inherit marginalia-size)) + "Face used to highlight minor mode lighters.") + +(defface marginalia-on + '((t :inherit success)) + "Face used to signal enabled modes.") + +(defface marginalia-off + '((t :inherit error)) + "Face used to signal disabled modes.") + +(defface marginalia-documentation + '((t :inherit completions-annotations)) + "Face used to highlight documentation strings.") + +(defface marginalia-value + '((t :inherit marginalia-key)) + "Face used to highlight general variable values.") + +(defface marginalia-null + '((t :inherit font-lock-comment-face)) + "Face used to highlight null or unbound variable values.") + +(defface marginalia-true + '((t :inherit font-lock-builtin-face)) + "Face used to highlight true variable values.") + +(defface marginalia-function + '((t :inherit font-lock-function-name-face)) + "Face used to highlight function symbols.") + +(defface marginalia-symbol + '((t :inherit font-lock-type-face)) + "Face used to highlight general symbols.") + +(defface marginalia-list + '((t :inherit font-lock-constant-face)) + "Face used to highlight list expressions.") + +(defface marginalia-mode + '((t :inherit marginalia-key)) + "Face used to highlight buffer major modes.") + +(defface marginalia-date + '((t :inherit marginalia-key)) + "Face used to highlight dates.") + +(defface marginalia-version + '((t :inherit marginalia-number)) + "Face used to highlight package versions.") + +(defface marginalia-archive + '((t :inherit warning)) + "Face used to highlight package archives.") + +(defface marginalia-installed + '((t :inherit success)) + "Face used to highlight the status of packages.") + +(defface marginalia-size + '((t :inherit marginalia-number)) + "Face used to highlight sizes.") + +(defface marginalia-number + '((t :inherit font-lock-constant-face)) + "Face used to highlight numeric values.") + +(defface marginalia-string + '((t :inherit font-lock-string-face)) + "Face used to highlight string values.") + +(defface marginalia-modified + '((t :inherit font-lock-negation-char-face)) + "Face used to highlight buffer modification indicators.") + +(defface marginalia-file-name + '((t :inherit marginalia-documentation)) + "Face used to highlight file names.") + +(defface marginalia-file-owner + '((t :inherit font-lock-preprocessor-face)) + "Face used to highlight file owner and group names.") + +(defface marginalia-file-priv-no + '((t :inherit shadow)) + "Face used to highlight the no file privilege attribute.") + +(defface marginalia-file-priv-dir + '((t :inherit font-lock-keyword-face)) + "Face used to highlight the dir file privilege attribute.") + +(defface marginalia-file-priv-link + '((t :inherit font-lock-keyword-face)) + "Face used to highlight the link file privilege attribute.") + +(defface marginalia-file-priv-read + '((t :inherit font-lock-type-face)) + "Face used to highlight the read file privilege attribute.") + +(defface marginalia-file-priv-write + '((t :inherit font-lock-builtin-face)) + "Face used to highlight the write file privilege attribute.") + +(defface marginalia-file-priv-exec + '((t :inherit font-lock-function-name-face)) + "Face used to highlight the exec file privilege attribute.") + +(defface marginalia-file-priv-other + '((t :inherit font-lock-constant-face)) + "Face used to highlight some other file privilege attribute.") + +(defface marginalia-file-priv-rare + '((t :inherit font-lock-variable-name-face)) + "Face used to highlight a rare file privilege attribute.") + +;;;; Pre-declarations for external packages + +(declare-function bookmark-prop-get "bookmark") + +(declare-function project-current "project") +(declare-function project-root "project") + +(defvar package--builtins) +(defvar package-archive-contents) +(declare-function package--from-builtin "package") +(declare-function package-desc-archive "package") +(declare-function package-desc-status "package") +(declare-function package-desc-summary "package") +(declare-function package-desc-version "package") +(declare-function package-version-join "package") + +(declare-function color-rgb-to-hex "color") +(declare-function color-rgb-to-hsl "color") +(declare-function color-hsl-to-rgb "color") + +;;;; Marginalia mode + +(defalias 'marginalia--orig-completion-metadata-get + (symbol-function + (if (fboundp 'marginalia--orig-completion-metadata-get) + 'marginalia--orig-completion-metadata-get + (compat-function completion-metadata-get))) + "Original `completion-metadata-get' function.") + +(defvar marginalia--pangram "Cwm fjord bank glyphs vext quiz.") + +(defvar marginalia--bookmark-type-transforms + (let ((words (regexp-opt '("handle" "handler" "jump" "bookmark")))) + `((,(format "-+%s-+" words) . "-") + (,(format "\\`%s-+" words) . "") + (,(format "-%s\\'" words) . "") + ("\\`default\\'" . "File") + (".*" . ,#'capitalize))) + "List of bookmark type transformers. +Relying on this mechanism is discouraged in favor of the +`bookmark-handler-type' property. The function names are matched +case-sensitively.") + +(defvar marginalia--cand-width-step 10 + "Round candidate width.") + +(defvar-local marginalia--cand-width-max 20 + "Maximum width of candidates.") + +(defvar marginalia--fontified-file-modes nil + "List of fontified file modes.") + +(defvar-local marginalia--cache nil + "The cache, pair of list and hashtable.") + +(defvar marginalia--cache-size 100 + "Size of the cache, set to 0 to disable the cache. +Disabling the cache is useful on non-incremental UIs like default completion or +for performance profiling of the annotators.") + +(defvar-local marginalia--command nil + "Last command symbol saved in order to allow annotations.") + +(defvar-local marginalia--base-position 0 + "Last completion base position saved to get full file paths.") + +(defvar marginalia--metadata nil + "Completion metadata from the current completion.") + +(defvar marginalia--ellipsis nil) +(defun marginalia--ellipsis () + "Return ellipsis." + (with-memoization marginalia--ellipsis + (cond + ((bound-and-true-p truncate-string-ellipsis)) + ((char-displayable-p ?…) "…") + ("...")))) + +(defun marginalia--abbreviate-file-name (file) + "Abbreviate FILE name without Tramp slowdown." + (let (file-name-handler-alist) + (abbreviate-file-name file))) + +(defun marginalia--truncate (str width) + "Truncate string STR to WIDTH." + (when (floatp width) (setq width (round (* width marginalia-field-width)))) + (when-let* ((pos (string-search "\n" str))) + (setq str (substring str 0 pos))) + (let* ((face (and (not (equal str "")) + (get-text-property (1- (length str)) 'face str))) + (ell (if face + (propertize (marginalia--ellipsis) 'face face) + (marginalia--ellipsis))) + (trunc + (if (< width 0) + (nreverse (truncate-string-to-width (reverse str) (- width) 0 ?\s ell)) + (truncate-string-to-width str width 0 ?\s ell)))) + (unless (string-prefix-p str trunc) + (put-text-property 0 (length trunc) 'help-echo str trunc)) + trunc)) + +(cl-defmacro marginalia--field (field &key truncate face width format) + "Format FIELD as a string according to some options. +TRUNCATE is the truncation width. +WIDTH is the field width. +FORMAT is a format string. +FACE is the name of the face, with which the field should be propertized." + (setq field (if format `(format ,format ,field) `(or ,field ""))) + (when width (setq field `(format ,(format "%%%ds" (- width)) ,field))) + (when truncate (setq field `(marginalia--truncate ,field ,truncate))) + (when face + (setq field (if (or format width truncate) + (cl-with-gensyms (f) + `(let ((,f ,field)) + (put-text-property 0 (length ,f) 'face ,face ,f) + ,f)) + `(propertize ,field 'face ,face)))) + field) + +(defmacro marginalia--fields (&rest fields) + "Format annotation FIELDS as a string with separators in between." + (let ((left t)) + (cons 'concat + (mapcan + (lambda (field) + (if (not (eq (car field) :left)) + `(,@(when left (setq left nil) `(#(" " 0 1 (marginalia--align t)))) + marginalia-separator (marginalia--field ,@field)) + (unless left (error "Left fields must come first")) + `((marginalia--field ,@(cdr field))))) + fields)))) + +(defmacro marginalia--in-minibuffer (&rest body) + "Run BODY inside minibuffer if minibuffer is active. +Otherwise stay within current buffer." + (declare (indent 0)) + `(with-current-buffer (if-let* ((win (active-minibuffer-window))) + (window-buffer win) + (current-buffer)) + ,@body)) + +(defun marginalia--documentation (str) + "Format documentation string STR." + (when str + (marginalia--fields + (str :truncate 1.0 :face 'marginalia-documentation)))) + +(defun marginalia-annotate-binding (cand) + "Annotate command CAND with keybinding." + (when-let* ((sym (intern-soft cand)) + (key (and (commandp sym) (where-is-internal sym nil 'first-only)))) + (format #(" (%s)" 1 5 (face marginalia-key)) (key-description key)))) + +(defun marginalia--annotator (cat) + "Return annotation function for category CAT." + (pcase (car (alist-get cat marginalia-annotators)) + ('none #'ignore) + ('builtin nil) + (fun fun))) + +(defun marginalia-annotate-multi-category (cand) + "Annotate multi-category CAND, dispatching to the appropriate annotator." + (if-let* ((multi (get-text-property 0 'multi-category cand)) + (fun (marginalia--annotator (car multi)))) + ;; Use the Marginalia annotator corresponding to the multi category. + (funcall fun (cdr multi)) + ;; Apply the original annotation function on the original candidate. Bypass + ;; our `marginalia--completion-metadata-get' advice. + (if-let* ((fun (marginalia--orig-completion-metadata-get + marginalia--metadata 'affixation-function))) + (caddar (funcall fun (list cand))) + (when-let* ((fun (marginalia--orig-completion-metadata-get + marginalia--metadata 'annotation-function))) + (funcall fun cand))))) + +(defconst marginalia--advice-regexp + (rx bos + (1+ (seq (? "This function has ") + (or ":before" ":after" ":around" ":override" + ":before-while" ":before-until" ":after-while" + ":after-until" ":filter-args" ":filter-return") + " advice: " (0+ nonl) "\n")) + "\n") + "Regexp to match lines about advice in function documentation strings.") + +;; Taken from advice--make-docstring, is this robust? +(defun marginalia--advised (fun) + "Return t if function FUN is advised." + (let ((flist (indirect-function fun))) + (advice--p (if (eq 'macro (car-safe flist)) (cdr flist) flist)))) + +(defun marginalia--symbol-class (s) + "Return symbol class characters for symbol S. + +This function is an extension of `help--symbol-class'. It returns +more fine-grained and more detailed symbol information. + +Function: +f function +c command +C interactive-only command +m macro +F special-form +M module function +P primitive +g cl-generic +p pure +s side-effect-free +@ autoloaded +! advised +- obsolete +& alias + +Variable: +u custom (U modified compared to global value) +v variable +l local (L modified compared to default value) +- obsolete +& alias + +Other: +G custom group +a face +t cl-type" + (let ((class + (append + (when (fboundp s) + (list + (cond + ((get s 'pure) '("p" . "pure")) + ((get s 'side-effect-free) '("s" . "side-effect-free"))) + (cond + ((commandp s) + (if (get s 'interactive-only) + '("C" . "interactive-only command") + '("c" . "command"))) + ((cl-generic-p s) '("g" . "cl-generic")) + ((macrop (symbol-function s)) '("m" . "macro")) + ((special-form-p (symbol-function s)) '("F" . "special-form")) + ((subr-primitive-p (symbol-function s)) '("P" . "primitive")) + ((module-function-p (symbol-function s)) '("M" . "module function")) + (t '("f" . "function"))) + (and (autoloadp (symbol-function s)) '("@" . "autoload")) + (and (marginalia--advised s) '("!" . "advised")) + (and (symbolp (symbol-function s)) + (cons "&" (format "alias for `%s'" (symbol-function s)))) + (and (get s 'byte-obsolete-info) '("-" . "obsolete")))) + (when (boundp s) + (list + (when (local-variable-if-set-p s) + (if (ignore-errors + (not (equal (symbol-value s) + (default-value s)))) + '("L" . "local, modified from global") + '("l" . "local, unmodified"))) + (if (get s 'standard-value) + (if (ignore-errors + (not (equal (symbol-value s) + (eval (car (get s 'standard-value)))))) + '("U" . "custom, modified from standard") + '("u" . "custom, unmodified")) + '("v" . "variable")) + (and (not (eq (ignore-errors (indirect-variable s)) s)) + (cons "&" (format "alias for `%s'" (ignore-errors (indirect-variable s))))) + (and (get s 'byte-obsolete-variable) '("-" . "obsolete")))) + (list + (and (get s 'group-documentation) '("G" . "custom group")) + (and (facep s) '("a" . "face")) + (and (get s 'cl--class) '("t" . "cl-type")))))) ;; cl-find-class, cl--find-class + (setq class (delq nil class)) + (propertize + (format "%-6s" (mapconcat #'car class "")) + 'help-echo + (mapconcat (pcase-lambda (`(,x . ,y)) (concat x " " y)) class "\n")))) + +(defun marginalia--definition-prefix (sym) + "Return annotation string if SYM is a definition prefix. +Sometimes symbols which are not yet loaded appear in completion tables +if `help-enable-completion-autoload' is enabled. These symbols +originate from the `definition-prefixes' hash table." + (when-let* (((bound-and-true-p help-enable-completion-autoload)) + (files (gethash (symbol-name sym) definition-prefixes))) + (format "[Not yet loaded from %s. See `help-enable-completion-autoload'.]" + (string-join files ", ")))) + +(defun marginalia--function-doc (sym) + "Documentation string of function SYM." + (if-let* ((str (ignore-errors (documentation sym)))) + (save-match-data + (if (string-match marginalia--advice-regexp str) + (substring str (match-end 0)) + str)) + (marginalia--definition-prefix sym))) + +;; Derived from elisp-get-fnsym-args-string +(defun marginalia--function-args (sym) + "Return function arguments for SYM." + (let (tmp) + (elisp-function-argstring + (cond + ((listp (setq tmp (gethash (indirect-function sym) + advertised-signature-table t))) + tmp) + ((setq tmp (help-split-fundoc + (ignore-errors (documentation sym t)) + sym)) + (car tmp)) + ((setq tmp (help-function-arglist sym)) + (if (and (stringp tmp) (string-search "not available" tmp)) + ;; A shorter text fits better into the limited Marginalia space. + "[autoload]" + tmp)))))) + +(defun marginalia-annotate-symbol (cand) + "Annotate symbol CAND with its documentation string." + (when-let* ((sym (intern-soft cand))) + (marginalia--fields + (:left (marginalia-annotate-binding cand)) + ((marginalia--symbol-class sym) :face 'marginalia-type) + ((if (fboundp sym) + (marginalia--function-doc sym) + (or (cl-loop + for doc in '(variable-documentation + face-documentation + group-documentation) + thereis (ignore-errors (documentation-property sym doc))) + (marginalia--definition-prefix sym))) + :truncate 1.0 :face 'marginalia-documentation) + ((marginalia--abbreviate-file-name (or (symbol-file sym) "")) + :truncate -0.5 :face 'marginalia-file-name)))) + +(defun marginalia-annotate-command (cand) + "Annotate command CAND with its documentation string. +Similar to `marginalia-annotate-symbol', but does not show symbol class." + (when-let* ((sym (intern-soft cand))) + (concat + (marginalia-annotate-binding cand) + (marginalia--documentation (marginalia--function-doc sym))))) + +(defun marginalia-annotate-embark-keybinding (cand) + "Annotate Embark keybinding CAND with its documentation string. +Similar to `marginalia-annotate-command', but does not show the +keybinding since CAND includes it." + (when-let* ((cmd (get-text-property 0 'embark-command cand)) + ((symbolp cmd))) + (marginalia--documentation (marginalia--function-doc cmd)))) + +(defun marginalia-annotate-imenu (cand) + "Annotate imenu CAND with its documentation string." + (when (derived-mode-p 'emacs-lisp-mode) + ;; Strip until the last whitespace in order to support flat imenu + (marginalia-annotate-symbol (replace-regexp-in-string "\\`.* " "" cand)))) + +(defun marginalia-annotate-function (cand) + "Annotate function CAND with its documentation string." + (when-let* ((sym (intern-soft cand))) + (marginalia--fields + (:left (marginalia-annotate-binding cand)) + ((marginalia--symbol-class sym) :face 'marginalia-type) + ((marginalia--function-args sym) :face 'marginalia-value + :truncate 0.5) + ((marginalia--function-doc sym) + :truncate 1.0 :face 'marginalia-documentation)))) + +(defun marginalia--variable-value (sym) + "Return the variable value of SYM as string." + (cond + ((not (boundp sym)) + (propertize "#<unbound>" 'face 'marginalia-null)) + ((and marginalia-censor-variables + (let ((name (symbol-name sym)) + case-fold-search) + (cl-loop for r in marginalia-censor-variables + thereis (if (symbolp r) + (eq r sym) + (string-match-p r name))))) + (propertize "*****" + 'face 'marginalia-null + 'help-echo "Hidden due to `marginalia-censor-variables'")) + (t + (let ((val (symbol-value sym))) + (pcase val + ('nil (propertize "nil" 'face 'marginalia-null)) + ('t (propertize "t" 'face 'marginalia-true)) + ((pred keymapp) (propertize "#<keymap>" 'face 'marginalia-value)) + ((pred bool-vector-p) (propertize "#<bool-vector>" 'face 'marginalia-value)) + ((pred hash-table-p) (propertize "#<hash-table>" 'face 'marginalia-value)) + ((pred syntax-table-p) (propertize "#<syntax-table>" 'face 'marginalia-value)) + ;; Emacs bug#53988: abbrev-table-p throws an error + ((guard (static-if (< emacs-major-version 30) + (and (vectorp val) (ignore-errors (abbrev-table-p val))) + (abbrev-table-p val))) + (propertize "#<abbrev-table>" 'face 'marginalia-value)) + ((pred char-table-p) (propertize "#<char-table>" 'face 'marginalia-value)) + ;; Callable objects or object closures (OClosures) + ((guard (oclosure-type val)) + (format (propertize "#<oclosure %s>" 'face 'marginalia-function) (oclosure-type val))) + ((pred byte-code-function-p) (propertize "#<byte-code-function>" 'face 'marginalia-function)) + ((and (pred functionp) (pred symbolp)) + ;; We are not consistent here, values are generally printed + ;; unquoted. But we make an exception for function symbols to visually + ;; distinguish them from symbols. I am not entirely happy with this, + ;; but we should not add quotation to every type. + (format (propertize "#'%s" 'face 'marginalia-function) val)) + ((pred recordp) (format (propertize "#<record %s>" 'face 'marginalia-value) (type-of val))) + ((pred symbolp) (propertize (symbol-name val) 'face 'marginalia-symbol)) + ((pred numberp) + (propertize (number-to-string val) + 'face 'marginalia-number + 'help-echo (and (integerp val) + (format "%d, #o%o, #x%x%s" val val val + (if (characterp val) (format ", ?%c" val) ""))))) + (_ (let ((print-escape-newlines t) + (print-escape-control-characters t) + ;;(print-escape-multibyte t) + (print-level 3) + (print-length marginalia-field-width)) + (propertize + (replace-regexp-in-string + ;; `print-escape-control-characters' does not escape Unicode control characters. + "[\x0-\x1F\x7f-\x9f\x061c\x200e\x200f\x202a-\x202e\x2066-\x2069]" + (lambda (x) (format "\\x%x" (string-to-char x))) + (prin1-to-string + (if (stringp val) + ;; Get rid of string properties to save some of the precious space + (substring-no-properties + val 0 + (min (length val) marginalia-field-width)) + val)) + 'fixedcase 'literal) + 'face + (cond + ((listp val) 'marginalia-list) + ((stringp val) 'marginalia-string) + (t 'marginalia-value)))))))))) + +(defun marginalia-annotate-variable (cand) + "Annotate variable CAND with its documentation string." + (when-let* ((sym (intern-soft cand))) + (marginalia--fields + ((marginalia--symbol-class sym) :face 'marginalia-type) + ((marginalia--variable-value sym) :truncate 0.5) + ((or (documentation-property sym 'variable-documentation) + (marginalia--definition-prefix sym)) + :truncate 1.0 :face 'marginalia-documentation)))) + +(defun marginalia-annotate-environment-variable (cand) + "Annotate environment variable CAND with its current value." + (when-let* ((val (getenv cand))) + (marginalia--fields + (val :truncate 1.0 :face 'marginalia-value)))) + +(defun marginalia-annotate-face (cand) + "Annotate face CAND with its documentation string and face example." + (when-let* ((sym (intern-soft cand))) + (marginalia--fields + ;; HACK: Manual alignment to fix misalignment due to face + ((concat marginalia--pangram #(" " 0 1 (display (space :align-to center)))) + :face sym) + ((documentation-property sym 'face-documentation) + :truncate 1.0 :face 'marginalia-documentation)))) + +(defun marginalia-annotate-color (cand) + "Annotate face CAND with its documentation string and face example." + (when-let* ((rgb (color-name-to-rgb cand))) + (pcase-let* ((`(,r ,g ,b) rgb) + (`(,h ,s ,l) (apply #'color-rgb-to-hsl rgb)) + (cr (color-rgb-to-hex r 0 0)) + (cg (color-rgb-to-hex 0 g 0)) + (cb (color-rgb-to-hex 0 0 b)) + (ch (apply #'color-rgb-to-hex (color-hsl-to-rgb h 1 0.5))) + (cs (apply #'color-rgb-to-hex (color-hsl-to-rgb h s 0.5))) + (cl (apply #'color-rgb-to-hex (color-hsl-to-rgb 0 0 l)))) + (marginalia--fields + (" " :face `(:background ,(apply #'color-rgb-to-hex rgb))) + ((format + "%s%s%s %s" + (propertize "r" 'face `(:background ,cr :foreground ,(readable-foreground-color cr))) + (propertize "g" 'face `(:background ,cg :foreground ,(readable-foreground-color cg))) + (propertize "b" 'face `(:background ,cb :foreground ,(readable-foreground-color cb))) + (color-rgb-to-hex r g b 2))) + ((format + "%s%s%s %3s° %3s%% %3s%%" + (propertize "h" 'face `(:background ,ch :foreground ,(readable-foreground-color ch))) + (propertize "s" 'face `(:background ,cs :foreground ,(readable-foreground-color cs))) + (propertize "l" 'face `(:background ,cl :foreground ,(readable-foreground-color cl))) + (round (* 360 h)) + (round (* 100 s)) + (round (* 100 l)))))))) + +(defun marginalia-annotate-char (cand) + "Annotate character CAND with its general character category and character code." + (when-let* ((char (char-from-name cand t))) + (marginalia--fields + (:left char :format" (%c)" :face 'marginalia-char) + (char :format "%06X" :face 'marginalia-number) + ((char-code-property-description + 'general-category + (get-char-code-property char 'general-category)) + :width 30 :face 'marginalia-documentation)))) + +(defun marginalia-annotate-minor-mode (cand) + "Annotate minor-mode CAND with status and documentation string." + (let* ((sym (intern-soft cand)) + (message-log-max nil) + (mode (if (and sym (boundp sym)) + sym + (lookup-minor-mode-from-indicator cand))) + (lighter (cdr (assq mode minor-mode-alist))) + (lighter-str (and lighter (string-trim (format-mode-line (cons t lighter)))))) + (marginalia--fields + ((if (and (boundp mode) (symbol-value mode)) + (propertize "On" 'face 'marginalia-on) + (propertize "Off" 'face 'marginalia-off)) :width 3) + ((if (local-variable-if-set-p mode) "Local" "Global") :width 6 :face 'marginalia-type) + (lighter-str :width 20 :face 'marginalia-lighter) + ((marginalia--function-doc mode) + :truncate 1.0 :face 'marginalia-documentation)))) + +(defun marginalia-annotate-package (cand) + "Annotate package CAND with its description summary." + (when-let* ((pkg-alist (bound-and-true-p package-alist)) + ;; See `package-get-version'. + (name (replace-regexp-in-string + "-[0-9]\\(?:[0-9.]\\|pre\\|beta\\|alpha\\|snapshot\\)+\\'" "" cand)) + (pkg (intern-soft name)) + (desc (or (unless (equal name cand) + (cl-loop with version = (substring cand (1+ (length name))) + for d in (alist-get pkg pkg-alist) + if (equal (package-version-join (package-desc-version d)) version) + return d)) + ;; taken from `describe-package-1' + (car (alist-get pkg pkg-alist)) + (if-let* ((built-in (assq pkg package--builtins))) + (package--from-builtin built-in) + (car (alist-get pkg package-archive-contents)))))) + (marginalia--fields + ((package-version-join (package-desc-version desc)) :truncate 16 :face 'marginalia-version) + ((cond + ((package-desc-archive desc) (propertize (package-desc-archive desc) 'face 'marginalia-archive)) + (t (propertize (or (package-desc-status desc) "orphan") 'face 'marginalia-installed))) :truncate 12) + ((package-desc-summary desc) :truncate 1.0 :face 'marginalia-documentation)))) + +(defun marginalia--bookmark-type (bm) + "Return bookmark type string of BM. +The string is transformed according to `marginalia--bookmark-type-transforms'." + (let ((handler (or (bookmark-prop-get bm 'handler) 'bookmark-default-handler))) + (and + ;; Some libraries use lambda handlers instead of symbols. For + ;; example the function `xwidget-webkit-bookmark-make-record' is + ;; affected. I consider this bad style since then the lambda is + ;; persisted. + (symbolp handler) + (or (get handler 'bookmark-handler-type) + (let ((str (symbol-name handler)) + case-fold-search) + (dolist (transformer marginalia--bookmark-type-transforms str) + (when (string-match-p (car transformer) str) + (setq str + (if (stringp (cdr transformer)) + (replace-regexp-in-string (car transformer) (cdr transformer) str) + (funcall (cdr transformer) str)))))))))) + +(defun marginalia-annotate-bookmark (cand) + "Annotate bookmark CAND with its file name and front context string." + (when-let* ((bm (assoc cand (bound-and-true-p bookmark-alist)))) + (marginalia--fields + ((marginalia--bookmark-type bm) :width 10 :face 'marginalia-type) + ((or (bookmark-prop-get bm 'filename) + (bookmark-prop-get bm 'location)) + :truncate (if (bookmark-prop-get bm 'filename) -0.5 0.5) + :face 'marginalia-file-name) + ((let ((front (or (bookmark-prop-get bm 'front-context-string) "")) + (rear (or (bookmark-prop-get bm 'rear-context-string) ""))) + (unless (and (string-blank-p front) (string-blank-p rear)) + (string-clean-whitespace + (concat front (marginalia--ellipsis) rear)))) + :truncate 0.5 :face 'marginalia-documentation)))) + +(defun marginalia-annotate-customize-group (cand) + "Annotate customization group CAND with its documentation string." + (marginalia--documentation (documentation-property (intern cand) 'group-documentation))) + +(defun marginalia-annotate-input-method (cand) + "Annotate input method CAND with its description." + (marginalia--documentation (nth 4 (assoc cand input-method-alist)))) + +(defun marginalia-annotate-charset (cand) + "Annotate charset CAND with its description." + (marginalia--documentation (charset-description (intern cand)))) + +(defun marginalia-annotate-coding-system (cand) + "Annotate coding system CAND with its description." + (marginalia--documentation (coding-system-doc-string (intern cand)))) + +(defun marginalia--buffer-status (buffer) + "Return the status of BUFFER as a string." + (format-mode-line '((:propertize "%1*%1+%1@" face marginalia-modified) + marginalia-separator + (7 (:propertize "%I" face marginalia-size)) + marginalia-separator + ;; InactiveMinibuffer has 18 letters, but there are longer names. + ;; For example Org-Agenda produces very long mode names. + ;; Therefore we have to truncate. + (20 (-20 (:propertize mode-name face marginalia-mode)))) + nil nil buffer)) + +(defun marginalia--buffer-file (buffer) + "Return the file or process name of BUFFER." + (if-let* ((proc (get-buffer-process buffer))) + (format "(%s %s) %s" + proc (process-status proc) + (marginalia--abbreviate-file-name (buffer-local-value 'default-directory buffer))) + (marginalia--abbreviate-file-name + (or (cond + ;; see ibuffer-buffer-file-name + ((buffer-file-name buffer)) + ((when-let* ((dir (and (local-variable-p 'dired-directory buffer) + (buffer-local-value 'dired-directory buffer)))) + (expand-file-name (if (stringp dir) dir (car dir)) + (buffer-local-value 'default-directory buffer)))) + ((local-variable-p 'list-buffers-directory buffer) + (buffer-local-value 'list-buffers-directory buffer))) + "")))) + +(defun marginalia-annotate-buffer (cand) + "Annotate buffer CAND with modification status, file name and major mode." + ;; Emacs 31: `project--read-project-buffer' uses `uniquify-get-unique-names' + (when-let* ((buffer (or (and (stringp cand) + (get-text-property 0 'uniquify-orig-buffer cand)) + (get-buffer cand)))) + (if (buffer-live-p buffer) + (marginalia--fields + ((marginalia--buffer-status buffer)) + ((marginalia--buffer-file buffer) + :truncate -0.5 :face 'marginalia-file-name)) + (marginalia--fields ("(dead buffer)" :face 'error))))) + +(defun marginalia--full-candidate (cand) + "Return completion candidate CAND in full. +For some completion tables, the completion candidates offered are +meant to be only a part of the full minibuffer contents. For +example, during file name completion the candidates are one path +component of a full file path." + (if-let* ((win (active-minibuffer-window))) + (with-current-buffer (window-buffer win) + (concat (let ((end (minibuffer-prompt-end))) + (buffer-substring-no-properties + end (+ end marginalia--base-position))) + cand)) + ;; no minibuffer is active, trust that cand already conveys all + ;; necessary information (there's not much else we can do) + cand)) + +(defun marginalia--remote-file-p (file) + "Return non-nil if FILE is remote. +The return value is a string describing the remote location, +e.g., the protocol." + (save-match-data + (setq file (let (file-name-handler-alist) + (substitute-in-file-name file))) + (cl-loop for r in marginalia-remote-file-regexps + if (string-match r file) + return (or (match-string 1 file) "remote")))) + +(defun marginalia--annotate-local-file (cand) + "Annotate local file CAND." + (marginalia--in-minibuffer + (when-let* ((attrs (ignore-errors + ;; may throw permission denied errors + (file-attributes (substitute-in-file-name + (marginalia--full-candidate cand)) + 'integer)))) + ;; HACK: Format differently accordingly to alignment, since the file owner + ;; is usually not displayed. Otherwise we will see an excessive amount of + ;; whitespace in front of the file permissions. Furthermore the alignment + ;; in `consult-buffer' will look ugly. Find a better solution! + (if (eq marginalia-align 'right) + (marginalia--fields + ;; File owner at the left + ((marginalia--file-owner attrs) :face 'marginalia-file-owner) + ((marginalia--file-modes attrs)) + ((marginalia--file-size attrs) :face 'marginalia-size :width -7) + ((marginalia--time (file-attribute-modification-time attrs)) + :face 'marginalia-date :width -12)) + (marginalia--fields + ((marginalia--file-modes attrs)) + ((marginalia--file-size attrs) :face 'marginalia-size :width -7) + ((marginalia--time (file-attribute-modification-time attrs)) + :face 'marginalia-date :width -12) + ;; File owner at the right + ((marginalia--file-owner attrs) :face 'marginalia-file-owner)))))) + +(defun marginalia-annotate-file (cand) + "Annotate file CAND with its size, modification time and other attributes. +These annotations are skipped for remote paths." + (if-let* ((remote (or (marginalia--remote-file-p cand) + (when-let* ((win (active-minibuffer-window))) + (with-current-buffer (window-buffer win) + (marginalia--remote-file-p (minibuffer-contents-no-properties))))))) + (marginalia--fields (remote :format "*%s*" :face 'marginalia-documentation)) + (marginalia--annotate-local-file cand))) + +(defun marginalia--file-owner (attrs) + "Return file owner given ATTRS." + (let ((uid (file-attribute-user-id attrs)) + (gid (file-attribute-group-id attrs))) + (when (or (/= (user-uid) uid) (/= (group-gid) gid)) + (format "%s:%s" + (or (user-login-name uid) uid) + (or (group-name gid) gid))))) + +(defun marginalia--file-size (attrs) + "Return formatted file size given ATTRS." + (propertize (file-size-human-readable (file-attribute-size attrs)) + 'help-echo (number-to-string (file-attribute-size attrs)))) + +(defun marginalia--file-modes (attrs) + "Return fontified file modes given the ATTRS." + ;; Without caching this can a be significant portion of the time + ;; `marginalia-annotate-file' takes to execute. Caching improves performance + ;; by about a factor of 20. + (setq attrs (file-attribute-modes attrs)) + (or (car (member attrs marginalia--fontified-file-modes)) + (progn + (setq attrs (substring attrs)) ;; copy because attrs is about to be modified + (dotimes (i (length attrs)) + (put-text-property + i (1+ i) 'face + (pcase (aref attrs i) + (?- 'marginalia-file-priv-no) + (?d 'marginalia-file-priv-dir) + (?l 'marginalia-file-priv-link) + (?r 'marginalia-file-priv-read) + (?w 'marginalia-file-priv-write) + (?x 'marginalia-file-priv-exec) + ((or ?s ?S ?t ?T) 'marginalia-file-priv-other) + (_ 'marginalia-file-priv-rare)) + attrs)) + (push attrs marginalia--fontified-file-modes) + attrs))) + +(defconst marginalia--time-relative + `((100 "sec" 1) + (,(* 60 100) "min" 60.0) + (,(* 3600 30) "hour" 3600.0) + (,(* 3600 24 400) "day" ,(* 3600.0 24.0)) + (nil "year" ,(* 365.25 24 3600))) + "Formatting used by the function `marginalia--time-relative'.") + +;; Taken from `seconds-to-string'. +(defun marginalia--time-relative (time) + "Format TIME as a relative age." + (setq time (max 0 (float-time (time-since time)))) + (let ((sts marginalia--time-relative) here) + (while (and (car (setq here (pop sts))) (<= (car here) time))) + (setq time (round time (caddr here))) + (format "%s %s%s ago" time (cadr here) (if (= time 1) "" "s")))) + +(defun marginalia--time-absolute (time) + "Format TIME as an absolute age." + (let ((system-time-locale "C")) + (format-time-string + (if (> (decoded-time-year (decode-time (current-time))) + (decoded-time-year (decode-time time))) + " %Y %b %d" + "%b %d %H:%M") + time))) + +(defun marginalia--time (time) + "Format file age TIME, suitably for use in annotations." + (propertize + (if (< (float-time (time-since time)) marginalia-max-relative-age) + (marginalia--time-relative time) + (marginalia--time-absolute time)) + 'help-echo (format-time-string "%Y-%m-%d %T" time))) + +(defvar-local marginalia--project-root 'unset) +(defun marginalia--project-root () + "Return project root." + (marginalia--in-minibuffer + (when (eq marginalia--project-root 'unset) + (setq marginalia--project-root + (or (let ((prompt (minibuffer-prompt)) + case-fold-search) + (and (string-match + "\\`\\(?:Dired\\|Find file\\) in \\(.*\\): \\'" + prompt) + (match-string 1 prompt))) + (when-let* ((proj (project-current))) + (project-root proj))))) + marginalia--project-root)) + +(defun marginalia-annotate-project-file (cand) + "Annotate file CAND with its size, modification time and other attributes." + ;; Absolute project directories also report project-file category + (if (file-name-absolute-p cand) + (marginalia-annotate-file cand) + (when-let* ((root (marginalia--project-root))) + (marginalia-annotate-file (expand-file-name cand root))))) + +(defvar-local marginalia--library-cache nil) +(defun marginalia--library-cache () + "Return hash table from library name to library file." + (marginalia--in-minibuffer + ;; `locate-file' and `locate-library' are bottlenecks for the + ;; annotator. Therefore we compute all the library paths first. + (unless marginalia--library-cache + (setq marginalia--library-cache (make-hash-table :test #'equal)) + (dolist (dir (delete-dups + (reverse ;; Reverse because of shadowing + (append load-path (custom-theme--load-path))))) ;; Include themes + (dolist (file (ignore-errors + (directory-files dir 'full + "\\.el\\(?:\\.gz\\)?\\'"))) + (puthash (marginalia--library-name file) + file marginalia--library-cache)))) + marginalia--library-cache)) + +(defun marginalia--library-name (file) + "Get name of library FILE." + (replace-regexp-in-string "\\(\\.gz\\|\\.elc?\\)+\\'" "" + (file-name-nondirectory file))) + +(defun marginalia--library-doc (file) + "Return library documentation string for FILE." + (let ((doc (get-text-property 0 'marginalia--library-doc file))) + (unless doc + ;; Extract documentation string. We cannot use `lm-summary' here, + ;; since it decompresses the whole file, which is slower. + (setq doc (or (ignore-errors + (let ((shell-file-name "sh") + (shell-command-switch "-c")) + (shell-command-to-string + (format (if (string-suffix-p ".gz" file) + "gzip -c -q -d %s | head -n1" + "head -n1 %s") + (shell-quote-argument file))))) + "")) + (cond + ((string-match "\\`(define-package\\s-+\"\\([^\"]+\\)\"" doc) + (setq doc (format "Generated package description from %s.el" + (match-string 1 doc)))) + ((string-match "\\`;+\\s-*" doc) + (setq doc (substring doc (match-end 0))) + (when (string-match "\\`[^ \t]+\\s-+-+\\s-+" doc) + (setq doc (substring doc (match-end 0)))) + (when (string-match "\\s-*-\\*-" doc) + (setq doc (substring doc 0 (match-beginning 0))))) + (t (setq doc ""))) + ;; Add the documentation string to the cache + (put-text-property 0 1 'marginalia--library-doc doc file)) + doc)) + +(defun marginalia-annotate-library (cand) + "Annotate library CAND with documentation and path." + (setq cand (marginalia--library-name cand)) + (when-let* ((file (gethash cand (marginalia--library-cache)))) + (marginalia--fields + ;; Display if the corresponding feature is loaded. + ;; feature/=library file, but better than nothing. + ((when-let* ((sym (intern-soft cand))) + (when (memq sym features) + (propertize "Loaded" 'face 'marginalia-on))) + :width 8) + ((marginalia--library-doc file) + :truncate 1.0 :face 'marginalia-documentation) + ((marginalia--abbreviate-file-name (file-name-directory file)) + :truncate -0.5 :face 'marginalia-file-name)))) + +(defun marginalia-annotate-theme (cand) + "Annotate theme CAND with documentation and path." + (when-let* ((file (gethash (concat cand "-theme") (marginalia--library-cache)))) + (marginalia--fields + ((marginalia--library-doc file) + :truncate 1.0 :face 'marginalia-documentation) + ((marginalia--abbreviate-file-name (file-name-directory file)) + :truncate -1.0 :face 'marginalia-file-name)))) + +(defun marginalia-annotate-frame (cand) + "Annotate frame named CAND with window and buffer information." + (when-let* ((frame (cl-loop + for f in (frame-list) + if (or (equal cand (frame-parameter f 'name)) + ;; `frame-id' is an Emacs 31 addition + (when (fboundp 'frame-id) + (equal cand (number-to-string (frame-id f))))) + return f))) + (let ((wins (window-list frame))) + (marginalia--fields + ((length wins) :format "win:%s" :face 'marginalia-size) + ((if (eq frame (selected-frame)) + "(current frame)" + (mapconcat (lambda (w) (buffer-name (window-buffer w))) wins " ")) + :face 'marginalia-documentation))))) + +(defun marginalia-annotate-tab (cand) + "Annotate named tab CAND with tab index, window and buffer information." + (when-let* ((tabs (funcall tab-bar-tabs-function)) + (index (seq-position + tabs nil + (lambda (tab _) (equal (alist-get 'name tab) cand))))) + (let* ((tab (nth index tabs)) + (ws (alist-get 'ws tab)) + (bufs (window-state-buffers ws))) + ;; When the buffer key is present in the window state it is added in front + ;; of the window buffer list and gets duplicated. + (when (cadr (assq 'buffer ws)) (pop bufs)) + (marginalia--fields + (:left (1+ index) :format " (%s)" :face 'marginalia-key) + ((if (eq (car tab) 'current-tab) + (length (window-list nil 'no-minibuf)) + (length bufs)) + :format "win:%s" :face 'marginalia-size) + ((or (alist-get 'group tab) 'none) + :format "group:%s" :face 'marginalia-type :truncate 20) + ((if (eq (car tab) 'current-tab) + "(current tab)" + (string-join bufs " ")) + :face 'marginalia-documentation))))) + +(defun marginalia-classify-by-command-name () + "Lookup category for current command." + (and marginalia--command + (or (alist-get marginalia--command marginalia-command-categories) + ;; The command can be an alias, e.g., `recentf' -> `recentf-open'. + (when-let* ((chain (function-alias-p marginalia--command))) + (alist-get (car (last chain)) marginalia-command-categories))))) + +(defun marginalia-classify-original-category () + "Return original category reported by completion metadata." + ;; Bypass our `marginalia--completion-metadata-get' advice. + (when-let* ((cat (marginalia--orig-completion-metadata-get marginalia--metadata 'category))) + ;; Ignore `symbol-help' category in order to ensure that the categories are + ;; refined to our categories function and variable. + (and (not (eq cat 'symbol-help)) cat))) + +(defun marginalia-classify-symbol () + "Determine if currently completing symbols." + (when-let* ((mct minibuffer-completion-table)) + (when (or (eq mct 'help--symbol-completion-table) + (obarrayp mct) + (and (not (functionp mct)) (consp mct) (symbolp (car mct)))) ; assume list of symbols + 'symbol))) + +(defun marginalia-classify-by-prompt () + "Determine category by matching regexps against the minibuffer prompt. +This runs through the `marginalia-prompt-categories' alist +looking for a regexp that matches the prompt." + (when-let* ((prompt (minibuffer-prompt))) + (setq prompt + (replace-regexp-in-string "(.*?default.*?)\\|\\[.*?\\]" "" prompt)) + (cl-loop with case-fold-search = t + for (regexp . category) in marginalia-prompt-categories + when (string-match-p regexp prompt) + return category))) + +(defun marginalia--cache-reset (&rest _) + "Reset the cache." + (setq marginalia--cache (and marginalia--cache (> marginalia--cache-size 0) + (cons nil (make-hash-table :test #'equal + :size marginalia--cache-size))))) + +(defun marginalia--cached (cache fun key) + "Cached application of function FUN with KEY. +The CACHE keeps around the last `marginalia--cache-size' computed +annotations. The cache is mainly useful when scrolling in +completion UIs like Vertico or Icomplete." + (if cache + (let ((ht (cdr cache))) + (or (gethash key ht) + (let ((val (funcall fun key))) + (push key (car cache)) + (puthash key val ht) + (when (>= (hash-table-count ht) marginalia--cache-size) + (let ((end (last (car cache) 2))) + (remhash (cadr end) ht) + (setcdr end nil))) + val))) + (funcall fun key))) + +(defun marginalia--align (cands) + "Align annotations of CANDS according to `marginalia-align'." + (cl-loop + for (cand . ann) in cands do + (when-let* ((align (text-property-any 0 (length ann) 'marginalia--align t ann))) + (setq marginalia--cand-width-max + (max marginalia--cand-width-max + (* (ceiling (+ (string-width cand) (string-width ann 0 align)) + marginalia--cand-width-step) + marginalia--cand-width-step))))) + (cl-loop + for (cand . ann) in cands collect + (progn + (when-let* ((align (text-property-any 0 (length ann) 'marginalia--align t ann))) + (put-text-property + align (1+ align) 'display + `(space :align-to + ,(pcase-exhaustive marginalia-align + ('center `(+ center ,marginalia-align-offset)) + ('left `(+ left ,(+ marginalia-align-offset marginalia--cand-width-max))) + ('right `(+ right ,(+ marginalia-align-offset 1 + (- (string-width ann 0 align) + (string-width ann))))))) + ann)) + (list cand "" ann)))) + +(defun marginalia--affixate (metadata annotator cands) + "Affixate CANDS given METADATA and Marginalia ANNOTATOR." + ;; Compute minimum width of windows, which display the minibuffer, including + ;; the miniwindow. In general the computed width corresponds to the full + ;; frame width, since the miniwindow spans the full frame. For example + ;; `vertico-buffer' displays the minibuffer in a separate window. Similarly, + ;; we could detect other types of completion buffers, e.g., Embark Collect or + ;; the default completion buffer, and compute smaller widths. + (let* ((width (cl-loop for win in (get-buffer-window-list) minimize (window-width win))) + (marginalia-field-width (min (/ width 2) marginalia-field-width)) + (marginalia--metadata metadata) + (cache marginalia--cache) + (orig-buf minibuffer--original-buffer)) + (marginalia--align + ;; Run the annotators in the original window. `with-selected-window' + ;; is necessary because of `lookup-minor-mode-from-indicator'. + ;; Otherwise it would suffice to only change the current buffer. We + ;; need the `selected-window' fallback for Embark Occur. + (with-selected-window (or (minibuffer-selected-window) (selected-window)) + (with-current-buffer (if (buffer-live-p orig-buf) orig-buf (current-buffer)) + (cl-loop for cand in cands collect + (let ((ann (or (marginalia--cached cache annotator cand) ""))) + (cons cand (if (string-blank-p ann) "" ann))))))))) + +(defun marginalia--completion-metadata-get (metadata prop) + "Meant as :before-until advice for `completion-metadata-get'. +METADATA is the metadata. +PROP is the property which is looked up." + (pcase prop + ('affixation-function + ;; We do want the advice triggered for `completion-metadata-get'. + (when-let* ((cat (completion-metadata-get metadata 'category)) + (annotator (marginalia--annotator cat))) + (apply-partially #'marginalia--affixate metadata annotator))) + ('category + ;; Find the completion category by trying each of our classifiers. + ;; Store the metadata for `marginalia-classify-original-category'. + (let ((marginalia--metadata metadata)) + (run-hook-with-args-until-success 'marginalia-classifiers))))) + +(defun marginalia--minibuffer-setup () + "Setup the minibuffer for Marginalia. +Remember `this-command' for `marginalia-classify-by-command-name'." + (setq marginalia--cache t marginalia--command this-command) + ;; Reset cache if window size changes, recompute alignment + (add-hook 'window-state-change-hook #'marginalia--cache-reset nil 'local) + (add-hook 'context-menu-functions #'marginalia--context-menu nil t) + (marginalia--cache-reset)) + +(defun marginalia--base-position (completions) + "Record the base position of COMPLETIONS." + ;; As a small optimization we track the base position only for file + ;; completions, since `marginalia--full-candidate' is currently used only by + ;; the file annotation function. + ;; bug#75910: category instead of `minibuffer-completing-file-name' + (when minibuffer-completing-file-name + (let ((base (or (cdr (last completions)) 0))) + (unless (= marginalia--base-position base) + (marginalia--cache-reset) + (setq marginalia--base-position base + marginalia--cand-width-max (default-value 'marginalia--cand-width-max))))) + completions) + +;;;###autoload +(define-minor-mode marginalia-mode + "Annotate completion candidates with richer information." + :global t :group 'marginalia + (if marginalia-mode + (progn + ;; Remember `this-command' in order to select the annotation function. + (add-hook 'minibuffer-setup-hook #'marginalia--minibuffer-setup) + ;; Replace the metadata function. + (advice-add (compat-function completion-metadata-get) :before-until #'marginalia--completion-metadata-get) + (advice-add #'completion-metadata-get :before-until #'marginalia--completion-metadata-get) + ;; Record completion base position, for `marginalia--full-candidate' + (advice-add #'completion-all-completions :filter-return #'marginalia--base-position)) + (advice-remove #'completion-all-completions #'marginalia--base-position) + (advice-remove (compat-function completion-metadata-get) #'marginalia--completion-metadata-get) + (advice-remove #'completion-metadata-get #'marginalia--completion-metadata-get) + (remove-hook 'minibuffer-setup-hook #'marginalia--minibuffer-setup))) + +(defun marginalia--completion-metadata () + "Get completion metadata." + (let* ((end (minibuffer-prompt-end)) + (pt (max 0 (- (point) end)))) + (completion-metadata (buffer-substring-no-properties end (+ end pt)) + minibuffer-completion-table + minibuffer-completion-predicate))) + +(defun marginalia--builtin-annotator-p (md) + "Builtin annotator available in metadata MD?" + (or (marginalia--orig-completion-metadata-get md 'annotation-function) + (marginalia--orig-completion-metadata-get md 'affixation-function))) + +;;;###autoload +(defun marginalia-cycle () + "Cycle between annotators in `marginalia-annotators'." + ;; Only show `marginalia-cycle' in M-x in recursive minibuffers + (declare (completion (lambda (&rest _) (> (minibuffer-depth) 1)))) + (interactive) + (with-current-buffer (window-buffer + (or (active-minibuffer-window) + (user-error "Marginalia: No active minibuffer"))) + (let* ((md (marginalia--completion-metadata)) + (cat (or (completion-metadata-get md 'category) + (user-error "Marginalia: Unknown completion category"))) + (ann (or (assq cat marginalia-annotators) + (user-error "Marginalia: No annotators found for category `%s'" cat)))) + (setcdr ann (append (cddr ann) (list (cadr ann)))) + ;; When the builtin annotator is selected and no builtin function is + ;; available, skip to the next annotator. Bypass the + ;; `marginalia--completion-metadata-get' advice. + (when (and (eq (cadr ann) 'builtin) (not (marginalia--builtin-annotator-p md))) + (setcdr ann (append (cddr ann) (list (cadr ann))))) + (marginalia--cache-reset) + (message "Marginalia: Use annotator `%s' for category `%s'" (cadr ann) cat)))) + +(defun marginalia--context-menu (menu _event) + "Add Marginalia commands to context MENU." + (when-let* ((md (marginalia--completion-metadata)) + (cat (completion-metadata-get md 'category)) + (ann (assq cat marginalia-annotators)) + (items (cl-loop + for fun in (cdr ann) for i from 0 + if (or (not (eq fun 'builtin)) (marginalia--builtin-annotator-p md)) + collect + (vector + (thread-last (symbol-name fun) + (replace-regexp-in-string ".*?-+annotate-+" "") + (replace-regexp-in-string "-+" " ") + capitalize) + (let ((i i)) + (lambda () + (interactive) + (setcdr ann (append (drop i (cdr ann)) (take i (cdr ann)))) + (marginalia--cache-reset) + (message "Marginalia: Use annotator `%s' for category `%s'" (cadr ann) cat))) + :style 'radio :selected (eq fun (cadr ann)))))) + (define-key menu [marginalia] + `("Marginalia" . ,(easy-menu-create-menu + "" `(["Cycle" marginalia-cycle] "---" ,@items))))) + menu) + +(provide 'marginalia) +;;; marginalia.el ends here diff --git a/.config/emacs/lisp/minadstack/orderless.el b/.config/emacs/lisp/minadstack/orderless.el new file mode 100644 index 0000000..7cee5c8 --- /dev/null +++ b/.config/emacs/lisp/minadstack/orderless.el @@ -0,0 +1,672 @@ +;;; orderless.el --- Completion style for matching regexps in any order -*- lexical-binding: t; -*- + +;; Copyright (C) 2021-2026 Free Software Foundation, Inc. + +;; Author: Omar Antolín Camarena <omar@matem.unam.mx> +;; Maintainer: Omar Antolín Camarena <omar@matem.unam.mx>, Daniel Mendler <mail@daniel-mendler.de> +;; Keywords: matching, completion +;; Version: 1.6 +;; URL: https://github.com/oantolin/orderless +;; Package-Requires: ((emacs "27.1") (compat "30")) + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This package provides an `orderless' completion style that divides +;; the pattern into components (space-separated by default), and +;; matches candidates that match all of the components in any order. + +;; Completion styles are used as entries in the variables +;; `completion-styles' and `completion-category-overrides', see their +;; documentation. + +;; To use this completion style you can use the following minimal +;; configuration: + +;; (setq completion-styles '(orderless basic)) + +;; You can customize the `orderless-component-separator' to decide how +;; the input pattern is split into component regexps. The default +;; splits on spaces. You might want to add hyphens and slashes, for +;; example, to ease completion of symbols and file paths, +;; respectively. + +;; Each component can match in any one of several matching styles: +;; literally, as a regexp, as an initialism, in the flex style, or as +;; word prefixes. It is easy to add new styles: they are functions +;; from strings to strings that map a component to a regexp to match +;; against. The variable `orderless-matching-styles' lists the +;; matching styles to be used for components, by default it allows +;; literal and regexp matching. + +;;; Code: + +(require 'compat) +(eval-when-compile (require 'cl-lib)) + +(defgroup orderless nil + "Completion method that matches space-separated regexps in any order." + :link '(info-link :tag "Info Manual" "(orderless)") + :link '(url-link :tag "Website" "https://github.com/oantolin/orderless") + :link '(emacs-library-link :tag "Library Source" "orderless.el") + :group 'minibuffer) + +(defface orderless-match-face-0 + '((default :weight bold) + (((class color) (min-colors 88) (background dark)) :foreground "#72a4ff") + (((class color) (min-colors 88) (background light)) :foreground "#223fbf") + (t :foreground "blue")) + "Face for matches of components numbered 0 mod 4.") + +(defface orderless-match-face-1 + '((default :weight bold) + (((class color) (min-colors 88) (background dark)) :foreground "#ed92f8") + (((class color) (min-colors 88) (background light)) :foreground "#8f0075") + (t :foreground "magenta")) + "Face for matches of components numbered 1 mod 4.") + +(defface orderless-match-face-2 + '((default :weight bold) + (((class color) (min-colors 88) (background dark)) :foreground "#90d800") + (((class color) (min-colors 88) (background light)) :foreground "#145a00") + (t :foreground "green")) + "Face for matches of components numbered 2 mod 4.") + +(defface orderless-match-face-3 + '((default :weight bold) + (((class color) (min-colors 88) (background dark)) :foreground "#f0ce43") + (((class color) (min-colors 88) (background light)) :foreground "#804000") + (t :foreground "yellow")) + "Face for matches of components numbered 3 mod 4.") + +(defcustom orderless-component-separator #'orderless-escapable-split-on-space + "Component separators for orderless completion. +This can either be a string, which is passed to `split-string', +or a function of a single string argument." + :type `(choice (const :tag "Spaces" " +") + (const :tag "Spaces, hyphen or slash" " +\\|[-/]") + (const :tag "Escapable space" + ,#'orderless-escapable-split-on-space) + (const :tag "Quotable spaces" ,#'split-string-and-unquote) + (regexp :tag "Custom regexp") + (function :tag "Custom function"))) + +(defcustom orderless-match-faces + [orderless-match-face-0 + orderless-match-face-1 + orderless-match-face-2 + orderless-match-face-3] + "Vector of faces used (cyclically) for component matches." + :type '(vector face)) + +(defcustom orderless-matching-styles + (list #'orderless-literal #'orderless-regexp) + "List of component matching styles. +If this variable is nil, regexp matching is assumed. + +A matching style is simply a function from strings to regexps. +The returned regexps can be either strings or s-expressions in +`rx' syntax. If the resulting regexp has no capturing groups, +the entire match is highlighted, otherwise just the captured +groups are. Several are provided with this package: try +customizing this variable to see a list of them." + :type '(repeat function) + :options (list #'orderless-regexp + #'orderless-literal + #'orderless-initialism + #'orderless-prefixes + #'orderless-flex)) + +(defcustom orderless-affix-dispatch-alist + `((?% . ,#'char-fold-to-regexp) + (?! . ,#'orderless-not) + (?& . ,#'orderless-annotation) + (?, . ,#'orderless-initialism) + (?= . ,#'orderless-literal) + (?^ . ,#'orderless-literal-prefix) + (?~ . ,#'orderless-flex)) + "Alist associating characters to matching styles. +The function `orderless-affix-dispatch' uses this list to +determine how to match a pattern component: if the component +either starts or ends with a character used as a key in this +alist, the character is removed from the component and the rest is +matched according the style associated to it." + :type `(alist + :key-type character + :value-type (choice + (const :tag "Annotation" ,#'orderless-annotation) + (const :tag "Literal" ,#'orderless-literal) + (const :tag "Without literal" ,#'orderless-without-literal) + (const :tag "Literal prefix" ,#'orderless-literal-prefix) + (const :tag "Regexp" ,#'orderless-regexp) + (const :tag "Not" ,#'orderless-not) + (const :tag "Flex" ,#'orderless-flex) + (const :tag "Initialism" ,#'orderless-initialism) + (const :tag "Prefixes" ,#'orderless-prefixes) + (const :tag "Ignore diacritics" ,#'char-fold-to-regexp) + (function :tag "Custom matching style")))) + +(defun orderless-affix-dispatch (component _index _total) + "Match COMPONENT according to the styles in `orderless-affix-dispatch-alist'. +If the COMPONENT starts or ends with one of the characters used +as a key in `orderless-affix-dispatch-alist', then that character +is removed and the remainder of the COMPONENT is matched in the +style associated to the character." + (let ((len (length component)) + (alist orderless-affix-dispatch-alist)) + (when (> len 0) + (cond + ;; Ignore single dispatcher character + ((and (= len 1) (alist-get (aref component 0) alist)) #'ignore) + ;; Prefix + ((when-let* ((style (alist-get (aref component 0) alist))) + (cons style (substring component 1)))) + ;; Suffix + ((when-let* ((style (alist-get (aref component (1- len)) alist))) + (cons style (substring component 0 -1)))))))) + +(defcustom orderless-style-dispatchers (list #'orderless-affix-dispatch) + "List of style dispatchers. +Style dispatchers are used to override the matching styles +based on the actual component and its place in the list of +components. A style dispatcher is a function that takes a string +and two integers as arguments, it gets called with a component, +the 0-based index of the component and the total number of +components. It can decide what matching styles to use for the +component and optionally replace the component with a different +string, or it can decline to handle the component leaving it for +future dispatchers. For details see `orderless--dispatch'. + +For example, a style dispatcher could arrange for the first +component to match as an initialism and subsequent components to +match as literals. As another example, a style dispatcher could +arrange for a component starting with `~' to match the rest of +the component in the `orderless-flex' style. See +`orderless-affix-dispatch' and `orderless-affix-dispatch-alist' +for such a configuration. For more information on how this +variable is used, see `orderless-compile'." + :type '(repeat function)) + +(defcustom orderless-smart-case t + "Whether to use smart case. +If this variable is t, then case-sensitivity is decided as +follows: if any component contains upper case letters, the +matches are case sensitive; otherwise case-insensitive. This +is like the behavior of `isearch' when `search-upper-case' is +non-nil. + +On the other hand, if this variable is nil, then case-sensitivity +is determined by the values of `completion-ignore-case', +`read-file-name-completion-ignore-case' and +`read-buffer-completion-ignore-case', as usual for completion." + :type 'boolean) + +(defcustom orderless-expand-substring 'prefix + "Whether to perform literal substring expansion. +This configuration option affects the behavior of some completion +interfaces when pressing TAB. If enabled `orderless-try-completion' +will first attempt literal substring expansion. If disabled, +expansion is only performed for single unique matches. For +performance reasons only `prefix' expansion is enabled by default. +Set the variable to `substring' for full substring expansion." + :type '(choice (const :tag "No expansion" nil) + (const :tag "Substring" substring) + (const :tag "Prefix (efficient)" prefix))) + +;;; Matching styles + +(defun orderless-regexp (component) + "Match COMPONENT as a regexp." + (condition-case nil + (progn (string-match-p component "") component) + (invalid-regexp nil))) + +(defun orderless-literal (component) + "Match COMPONENT as a literal string." + ;; Do not use (literal component) here, such that `delete-dups' in + ;; `orderless--compile-component' has a chance to delete duplicates for + ;; literal input. The default configuration of `orderless-matching-styles' + ;; with `orderless-regexp' and `orderless-literal' leads to duplicates. + (regexp-quote component)) + +(defun orderless-literal-prefix (component) + "Match COMPONENT as a literal prefix string." + `(seq bos (literal ,component))) + +(defun orderless--separated-by (sep rxs &optional before after) + "Return a regexp to match the rx-regexps RXS with SEP in between. +If BEFORE is specified, add it to the beginning of the rx +sequence. If AFTER is specified, add it to the end of the rx +sequence." + (declare (indent 1)) + `(seq + ,(or before "") + ,@(cl-loop for (sexp . more) on rxs + collect `(group ,sexp) + when more collect sep) + ,(or after ""))) + +(defun orderless-flex (component) + "Match a component in flex style. +This means the characters in COMPONENT must occur in the +candidate in that order, but not necessarily consecutively." + `(seq + ,@(cdr (cl-loop for char across component + append `((zero-or-more (not ,char)) (group ,char)))))) + +(defun orderless-initialism (component) + "Match a component as an initialism. +This means the characters in COMPONENT must occur in the +candidate, in that order, at the beginning of words." + (orderless--separated-by '(zero-or-more nonl) + (cl-loop for char across component collect `(seq word-start ,char)))) + +(defun orderless-prefixes (component) + "Match a component as multiple word prefixes. +The COMPONENT is split at word endings, and each piece must match +at a word boundary in the candidate. This is similar to the +`partial-completion' completion style." + (orderless--separated-by '(zero-or-more nonl) + (cl-loop for prefix in (split-string component "\\>") + collect `(seq word-boundary ,prefix)))) + +(defun orderless-without-literal (component) + "Match strings that do *not* contain COMPONENT as a literal match. +You may prefer to use the more general `orderless-not' instead +which can invert any predicate or regexp." + `(seq + (group string-start) ; highlight nothing! + (zero-or-more + (or ,@(cl-loop for i below (length component) + collect `(seq ,(substring component 0 i) + (or (not (any ,(aref component i))) + string-end))))) + string-end)) + +(defsubst orderless--match-p (pred regexp str) + "Return t if STR matches PRED and REGEXP." + (and str + (or (not pred) (funcall pred str)) + (or (not regexp) + (let ((case-fold-search completion-ignore-case)) + (string-match-p regexp str))))) + +(defun orderless-not (pred regexp) + "Match strings that do *not* match PRED and REGEXP." + (lambda (str) + (not (orderless--match-p pred regexp str)))) + +(defun orderless--metadata () + "Return completion metadata iff inside minibuffer." + (when-let* (((minibufferp)) + (table minibuffer-completion-table)) + ;; Return non-nil metadata iff inside minibuffer + (or (completion-metadata (buffer-substring-no-properties + (minibuffer-prompt-end) (point)) + table minibuffer-completion-predicate) + '((nil . nil))))) + +(defun orderless-annotation (pred regexp) + "Match candidates where the annotation matches PRED and REGEXP." + (let ((md (orderless--metadata))) + (if-let* ((fun (compat-call completion-metadata-get md 'affixation-function))) + (lambda (str) + (cl-loop for s in (cdar (funcall fun (list str))) + thereis (orderless--match-p pred regexp s))) + (when-let* ((fun (compat-call completion-metadata-get md 'annotation-function))) + (lambda (str) (orderless--match-p pred regexp (funcall fun str))))))) + +;;; Highlighting matches + +(defun orderless--highlight (regexps ignore-case string) + "Destructively propertize STRING to highlight a match of each of the REGEXPS. +The search is case insensitive if IGNORE-CASE is non-nil." + (cl-loop with case-fold-search = ignore-case + with n = (length orderless-match-faces) + for regexp in regexps and i from 0 + when (string-match regexp string) do + (cl-loop + for (x y) on (let ((m (match-data))) (or (cddr m) m)) by #'cddr + when x do + (add-face-text-property + x y + (aref orderless-match-faces (mod i n)) + nil string))) + string) + +(defun orderless-highlight-matches (regexps strings) + "Highlight a match of each of the REGEXPS in each of the STRINGS. +Warning: only use this if you know all REGEXPs match all STRINGS! +For the user's convenience, if REGEXPS is a string, it is +converted to a list of regexps according to the value of +`orderless-matching-styles'." + (when (stringp regexps) + (setq regexps (cdr (orderless-compile regexps)))) + (cl-loop with ignore-case = (orderless--ignore-case-p regexps) + for str in strings + collect (orderless--highlight regexps ignore-case (substring str)))) + +;;; Compiling patterns to lists of regexps + +(defun orderless-escapable-split-on-space (string) + "Split STRING on spaces, which can be escaped with backslash." + (mapcar + (lambda (piece) (replace-regexp-in-string (string 0) " " piece)) + (split-string (replace-regexp-in-string + "\\\\\\\\\\|\\\\ " + (lambda (x) (if (equal x "\\ ") (string 0) x)) + string 'fixedcase 'literal) + " +"))) + +(defun orderless--dispatch (dispatchers default string index total) + "Run DISPATCHERS to compute matching styles for STRING. + +A style dispatcher is a function that takes a STRING, component +INDEX and the TOTAL number of components. It should either +return (a) nil to indicate the dispatcher will not handle the +string, (b) a new string to replace the current string and +continue dispatch, or (c) the matching styles to use and, if +needed, a new string to use in place of the current one (for +example, a dispatcher can decide which style to use based on a +suffix of the string and then it must also return the component +stripped of the suffix). + +More precisely, the return value of a style dispatcher can be of +one of the following forms: + +- nil (to continue dispatching) + +- a string (to replace the component and continue dispatching), + +- a matching style or non-empty list of matching styles to + return, + +- a `cons' whose `car' is either as in the previous case or + nil (to request returning the DEFAULT matching styles), and + whose `cdr' is a string (to replace the current one). + +This function tries all DISPATCHERS in sequence until one returns +a list of styles. When that happens it returns a `cons' of the +list of styles and the possibly updated STRING. If none of the +DISPATCHERS returns a list of styles, the return value will use +DEFAULT as the list of styles." + (cl-loop for dispatcher in dispatchers + for result = (funcall dispatcher string index total) + if (stringp result) + do (setq string result result nil) + else if (and (consp result) (null (car result))) + do (setf (car result) default) + else if (and (consp result) (stringp (cdr result))) + do (setq string (cdr result) result (car result)) + when result return (cons result string) + finally (return (cons default string)))) + +(defun orderless--compile-component (component index total styles dispatchers) + "Compile COMPONENT at INDEX of TOTAL components with STYLES and DISPATCHERS." + (cl-loop + with pred = nil + with (newsty . newcomp) = (orderless--dispatch dispatchers styles + component index total) + for style in (if (functionp newsty) (list newsty) newsty) + for res = (condition-case nil + (funcall style newcomp) + (wrong-number-of-arguments + (when-let* ((res (orderless--compile-component + newcomp index total styles dispatchers))) + (funcall style (car res) (cdr res))))) + if (functionp res) do (cl-callf orderless--predicate-and pred res) + else if res collect (if (stringp res) `(regexp ,res) res) into regexps + finally return + (when (or pred regexps) + (cons pred (and regexps (rx-to-string `(or ,@(delete-dups regexps)) t)))))) + +(defun orderless-compile (pattern &optional styles dispatchers) + "Build regexps to match the components of PATTERN. +Split PATTERN on `orderless-component-separator' and compute +matching styles for each component. For each component the style +DISPATCHERS are run to determine the matching styles to be used; +they are called with arguments the component, the 0-based index +of the component and the total number of components. If the +DISPATCHERS decline to handle the component, then the list of +matching STYLES is used. See `orderless--dispatch' for details +on dispatchers. + +The STYLES default to `orderless-matching-styles', and the +DISPATCHERS default to `orderless-dipatchers'. Since nil gets +you the default, if you want no dispatchers to be run, use +\\='(ignore) as the value of DISPATCHERS. + +The return value is a pair of a predicate function and a list of +regexps. The predicate function can also be nil. It takes a +string as argument." + (unless styles (setq styles orderless-matching-styles)) + (unless dispatchers (setq dispatchers orderless-style-dispatchers)) + (cl-loop + with predicate = nil + with temp = (if (functionp orderless-component-separator) + (funcall orderless-component-separator pattern) + (split-string pattern orderless-component-separator)) + with components = (if (equal (car (last temp)) "") (nbutlast temp) temp) + with total = (length components) + for comp in components and index from 0 + for (pred . regexp) = (orderless--compile-component + comp index total styles dispatchers) + when regexp collect regexp into regexps + when pred do (cl-callf orderless--predicate-and predicate pred) + finally return (cons predicate regexps))) + +;;; Completion style implementation + +(defun orderless--predicate-normalized-and (p q) + "Combine two predicate functions P and Q with `and'. +The first function P is a completion predicate which can receive +up to two arguments. The second function Q always receives a +normalized string as argument." + (cond + ((and p q) + (lambda (k &rest v) ;; v for hash table + (when (if v (funcall p k (car v)) (funcall p k)) + (setq k (if (consp k) (car k) k)) ;; alist + (funcall q (if (symbolp k) (symbol-name k) k))))) + (q + (lambda (k &optional _) ;; _ for hash table + (setq k (if (consp k) (car k) k)) ;; alist + (funcall q (if (symbolp k) (symbol-name k) k)))) + (p))) + +(defun orderless--predicate-and (p q) + "Combine two predicate functions P and Q with `and'." + (or (and p q (lambda (x) (and (funcall p x) (funcall q x)))) p q)) + +(defun orderless--compile (string table pred) + "Compile STRING to a prefix and a list of regular expressions. +The predicate PRED is used to constrain the entries in TABLE." + (pcase-let* ((limit (car (completion-boundaries string table pred ""))) + (prefix (substring string 0 limit)) + (pattern (substring string limit)) + (`(,fun . ,regexps) (orderless-compile pattern))) + (list prefix regexps (orderless--ignore-case-p pattern) + (orderless--predicate-normalized-and pred fun)))) + +;; Thanks to @jakanakaevangeli for writing a version of this function: +;; https://github.com/oantolin/orderless/issues/79#issuecomment-916073526 +(defun orderless--literal-prefix-p (regexp) + "Determine if REGEXP is a quoted regexp anchored at the beginning. +If REGEXP is of the form \"\\`q\" for q = (regexp-quote u), +then return (cons REGEXP u); else return nil." + (when (and (string-prefix-p "\\`" regexp) + (not (string-match-p "[$*+.?[\\^]" + (replace-regexp-in-string + "\\\\[$*+.?[\\^]" "" regexp + 'fixedcase 'literal nil 2)))) + (cons regexp + (replace-regexp-in-string "\\\\\\([$*+.?[\\^]\\)" "\\1" + regexp 'fixedcase nil nil 2)))) + +(defun orderless--ignore-case-p (regexps) + "Return non-nil if case should be ignored for REGEXPS." + (if orderless-smart-case + (cl-loop for regexp in (ensure-list regexps) + always (isearch-no-upper-case-p regexp t)) + completion-ignore-case)) + +(defun orderless--filter (prefix regexps ignore-case table pred) + "Filter TABLE by PREFIX, REGEXPS and PRED. +The matching should be case-insensitive if IGNORE-CASE is non-nil." + ;; If there is a regexp of the form \`quoted-regexp then + ;; remove the first such and add the unquoted form to the prefix. + (pcase (cl-loop for r in regexps + thereis (orderless--literal-prefix-p r)) + (`(,regexp . ,literal) + (setq prefix (concat prefix literal) + regexps (remove regexp regexps)))) + (let ((completion-regexp-list regexps) + (completion-ignore-case ignore-case)) + (all-completions prefix table pred))) + +(defun orderless-filter (string table &optional pred) + "Split STRING into components and find entries TABLE matching all. +The predicate PRED is used to constrain the entries in TABLE." + (pcase-let ((`(,prefix ,regexps ,ignore-case ,pred) + (orderless--compile string table pred))) + (orderless--filter prefix regexps ignore-case table pred))) + +;;;###autoload +(defun orderless-all-completions (string table pred _point) + "Split STRING into components and find entries TABLE matching all. +The predicate PRED is used to constrain the entries in TABLE. The +matching portions of each candidate are highlighted. +This function is part of the `orderless' completion style." + (pcase-let ((`(,prefix ,regexps ,ignore-case ,pred) + (orderless--compile string table pred))) + (when-let* ((completions (orderless--filter prefix regexps ignore-case table pred))) + (if completion-lazy-hilit + (setq completion-lazy-hilit-fn + (apply-partially #'orderless--highlight regexps ignore-case)) + (cl-loop for str in-ref completions do + (setf str (orderless--highlight regexps ignore-case (substring str))))) + (nconc completions (length prefix))))) + +;;;###autoload +(defun orderless-try-completion (string table pred point) + "Complete STRING to unique matching entry in TABLE. +This uses `orderless-all-completions' to find matches for STRING +in TABLE among entries satisfying PRED. If there is only one +match, it completes to that match. If there are no matches, it +returns nil. In any other case it \"completes\" STRING to +itself, without moving POINT. +This function is part of the `orderless' completion style." + (or + (pcase orderless-expand-substring + ('nil nil) + ('prefix (completion-emacs21-try-completion string table pred point)) + (_ (completion-substring-try-completion string table pred point))) + (catch 'orderless--many + (pcase-let ((`(,prefix ,regexps ,ignore-case ,pred) + (orderless--compile string table pred)) + (one nil)) + ;; Abuse all-completions/orderless--filter as a fast search loop. + ;; Should be almost allocation-free since our "predicate" is not + ;; called more than two times. + (orderless--filter + prefix regexps ignore-case table + (orderless--predicate-normalized-and + pred + (lambda (arg) + ;; Check if there is more than a single match (= many). + (when (and one (not (equal one arg))) + (throw 'orderless--many (cons string point))) + (setq one arg) + t))) + (when one + ;; Prepend prefix if the candidate does not already have the same + ;; prefix. This workaround is needed since the predicate may either + ;; receive an unprefixed or a prefixed candidate as argument. Most + ;; completion tables consistently call the predicate with unprefixed + ;; candidates, for example `completion-file-name-table'. In contrast, + ;; `completion-table-with-context' calls the predicate with prefixed + ;; candidates. This could be an unintended bug or oversight in + ;; `completion-table-with-context'. + (unless (or (equal prefix "") + (and (string-prefix-p prefix one) + (test-completion one table pred))) + (setq one (concat prefix one))) + (or (equal string one) ;; Return t for unique exact match + (cons one (length one)))))))) + +;;;###autoload +(add-to-list 'completion-styles-alist + '(orderless + orderless-try-completion orderless-all-completions + "Completion of multiple components, in any order.")) + +(defmacro orderless-define-completion-style + (name &optional docstring &rest configuration) + "Define an orderless completion style with given CONFIGURATION. +The CONFIGURATION should be a list of bindings that you could use +with `let' to configure orderless. You can include bindings for +`orderless-matching-styles' and `orderless-style-dispatchers', +for example. + +The completion style consists of two functions that this macro +defines for you, NAME-try-completion and NAME-all-completions. +This macro registers those in `completion-styles-alist' as +forming the completion style NAME. + +The optional DOCSTRING argument is used as the documentation +string for the completion style." + (declare (doc-string 2) (indent 1)) + (unless (stringp docstring) + (push docstring configuration) + (setq docstring nil)) + (let* ((fn-name (lambda (string) (intern (concat (symbol-name name) string)))) + (try-completion (funcall fn-name "-try-completion")) + (all-completions (funcall fn-name "-all-completions")) + (doc-fmt "`%s' function for the %s style. +This function delegates to `orderless-%s'. +The orderless configuration is locally modified +specifically for the %s style.") + (fn-doc (lambda (fn) (format doc-fmt fn name fn name name)))) + `(progn + (defun ,try-completion (string table pred point) + ,(funcall fn-doc "try-completion") + (let ,configuration + (orderless-try-completion string table pred point))) + (defun ,all-completions (string table pred point) + ,(funcall fn-doc "all-completions") + (let ,configuration + (orderless-all-completions string table pred point))) + (add-to-list 'completion-styles-alist + '(,name ,try-completion ,all-completions ,docstring))))) + +;;; Ivy integration + +;;;###autoload +(defun orderless-ivy-re-builder (str) + "Convert STR into regexps for use with ivy. +This function is for integration of orderless with ivy, use it as +a value in `ivy-re-builders-alist'." + (or (mapcar (lambda (x) (cons x t)) (cdr (orderless-compile str))) "")) + +(defvar ivy-regex) +(defun orderless-ivy-highlight (str) + "Highlight a match in STR of each regexp in `ivy-regex'. +This function is for integration of orderless with ivy." + (orderless--highlight (mapcar #'car ivy-regex) t str) str) + +(provide 'orderless) +;;; orderless.el ends here diff --git a/.config/emacs/lisp/minadstack/vertico-directory.el b/.config/emacs/lisp/minadstack/vertico-directory.el new file mode 100644 index 0000000..3328dd1 --- /dev/null +++ b/.config/emacs/lisp/minadstack/vertico-directory.el @@ -0,0 +1,136 @@ +;;; vertico-directory.el --- Ido-like directory navigation for Vertico -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2026 Free Software Foundation, Inc. + +;; Author: Daniel Mendler <mail@daniel-mendler.de> +;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> +;; Created: 2021 +;; Version: 2.8 +;; Package-Requires: ((emacs "29.1") (compat "30") (vertico "2.8")) +;; URL: https://github.com/minad/vertico + +;; This file is part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This package is a Vertico extension, which provides Ido-like +;; directory navigation commands. The commands can be bound in the +;; `vertico-map'. +;; +;; (keymap-set vertico-map "RET" #'vertico-directory-enter) +;; (keymap-set vertico-map "DEL" #'vertico-directory-delete-char) +;; (keymap-set vertico-map "M-DEL" #'vertico-directory-delete-word) +;; +;; Alternatively use `vertico-directory-map' together with +;; `vertico-multiform-mode'. +;; +;; (setq vertico-multiform-categories +;; '((file (:keymap . vertico-directory-map)))) +;; (vertico-multiform-mode) +;; +;; Furthermore a cleanup function for shadowed file paths is provided. +;; +;; (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy) + +;;; Code: + +(require 'vertico) +(eval-when-compile (require 'subr-x)) + +;;;###autoload +(defun vertico-directory-enter (&optional arg) + "Enter directory or exit completion with current candidate. +Exit with current input if prefix ARG is given." + (interactive "P") + (if-let* (((not arg)) + ((>= vertico--index 0)) + ((eq 'file (vertico--metadata-get 'category))) + ;; Check vertico--base for stepwise file path completion + ((not (equal vertico--base ""))) + (cand (vertico--candidate)) + ((or (string-suffix-p "/" cand) + (and (vertico--remote-p cand) + (string-suffix-p ":" cand)))) + ;; Handle /./ and /../ manually instead of via `expand-file-name' + ;; and `abbreviate-file-name', such that we don't accidentally + ;; perform unwanted substitutions in the existing completion. + ((progn + (setq cand (string-replace "/./" "/" cand)) + (unless (string-suffix-p "/../../" cand) + (setq cand (replace-regexp-in-string "/[^/|:]+/\\.\\./\\'" "/" cand))) + (not (equal (minibuffer-contents-no-properties) cand))))) + (progn + (delete-minibuffer-contents) + (insert cand)) + (vertico-exit arg))) + +;;;###autoload +(defun vertico-directory-up (&optional n) + "Delete N names before point." + (interactive "p") + (when (and (> (point) (minibuffer-prompt-end)) + (eq 'file (vertico--metadata-get 'category))) + (let ((path (buffer-substring-no-properties (minibuffer-prompt-end) (point))) + found) + (when (string-match-p "\\`~[^/]*/\\'" path) + (delete-minibuffer-contents) + (insert (expand-file-name path))) + (dotimes (_ (or n 1) found) + (save-excursion + (let ((end (point))) + (goto-char (1- end)) + (when (search-backward "/" (minibuffer-prompt-end) t) + (delete-region (1+ (point)) end) + (setq found t)))))))) + +;;;###autoload +(defun vertico-directory-delete-char (n) + "Delete N directories or chars before point." + (interactive "p") + (unless (and (not (and (use-region-p) delete-active-region (= n 1))) + (eq (char-before) ?/) (vertico-directory-up n)) + (with-no-warnings (delete-backward-char n)))) + +;;;###autoload +(defun vertico-directory-delete-word (n) + "Delete N directories or words before point." + (interactive "p") + (unless (and (eq (char-before) ?/) (vertico-directory-up n)) + (delete-region (prog1 (point) (backward-word n)) (point)))) + +;;;###autoload +(defun vertico-directory-tidy () + "Tidy shadowed file name, see `rfn-eshadow-overlay'." + (when (eq this-command #'self-insert-command) + (dolist (ov '(tramp-rfn-eshadow-overlay rfn-eshadow-overlay)) + (when (and (boundp ov) + (setq ov (symbol-value ov)) + (overlay-buffer ov) + (= (point) (point-max)) + (> (point) (overlay-end ov))) + (delete-region (overlay-start ov) (overlay-end ov)))))) + +(defvar-keymap vertico-directory-map + :doc "File name editing map." + "RET" #'vertico-directory-enter + "DEL" #'vertico-directory-delete-char + "M-DEL" #'vertico-directory-delete-word) + +;;;###autoload (autoload 'vertico-directory-map "vertico-directory" nil t 'keymap) +(defalias 'vertico-directory-map vertico-directory-map) + +(provide 'vertico-directory) +;;; vertico-directory.el ends here diff --git a/.config/emacs/lisp/minadstack/vertico.el b/.config/emacs/lisp/minadstack/vertico.el new file mode 100644 index 0000000..c646f0b --- /dev/null +++ b/.config/emacs/lisp/minadstack/vertico.el @@ -0,0 +1,736 @@ +;;; vertico.el --- VERTical Interactive COmpletion -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2026 Free Software Foundation, Inc. + +;; Author: Daniel Mendler <mail@daniel-mendler.de> +;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> +;; Created: 2021 +;; Version: 2.8 +;; Package-Requires: ((emacs "29.1") (compat "30")) +;; URL: https://github.com/minad/vertico +;; Keywords: convenience, files, matching, completion + +;; This file is part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Vertico provides a performant and minimalistic vertical completion UI +;; based on the default completion system. By reusing the built-in +;; facilities, Vertico achieves full compatibility with built-in Emacs +;; completion commands and completion tables. + +;;; Code: + +(require 'compat) +(eval-when-compile + (require 'cl-lib) + (require 'subr-x)) + +(defgroup vertico nil + "VERTical Interactive COmpletion." + :link '(info-link :tag "Info Manual" "(vertico)") + :link '(url-link :tag "Website" "https://github.com/minad/vertico") + :link '(url-link :tag "Wiki" "https://github.com/minad/vertico/wiki") + :link '(emacs-library-link :tag "Library Source" "vertico.el") + :group 'convenience + :group 'minibuffer + :prefix "vertico-") + +(defcustom vertico-count-format (cons "%-6s " "%s/%s") + "Format string used for the candidate count." + :type '(choice (const :tag "No candidate count" nil) (cons string string))) + +(defcustom vertico-group-format + (concat #(" " 0 4 (face vertico-group-separator)) + #(" %s " 0 4 (face vertico-group-title)) + #(" " 0 1 (face vertico-group-separator display (space :align-to (- right 1))))) + "Format string used for the group title." + :type '(choice (const :tag "No group titles" nil) string)) + +(defcustom vertico-count 10 + "Maximal number of candidates to show." + :type 'natnum) + +(defcustom vertico-preselect 'directory + "Configure if the prompt or first candidate is preselected. +- prompt: Always select the prompt. +- first: Select the first candidate, allow prompt selection. +- no-prompt: Like first, but forbid selection of the prompt entirely. +- directory: Like first, but select the prompt if it is a directory." + :type '(choice (const prompt) (const first) (const no-prompt) (const directory))) + +(defcustom vertico-scroll-margin 2 + "Number of lines at the top and bottom when scrolling. +The value should lie between 0 and vertico-count/2." + :type 'natnum) + +(defcustom vertico-resize resize-mini-windows + "How to resize the Vertico minibuffer window, see `resize-mini-windows'." + :type '(choice (const :tag "Fixed" nil) + (const :tag "Shrink and grow" t) + (const :tag "Grow-only" grow-only))) + +(defcustom vertico-cycle nil + "Enable cycling for `vertico-next' and `vertico-previous'." + :type 'boolean) + +(defcustom vertico-multiline + (cons #("↲" 0 1 (face vertico-multiline)) #("…" 0 1 (face vertico-multiline))) + "Replacements for multiline strings." + :type '(cons (string :tag "Newline") (string :tag "Truncation"))) + +(defcustom vertico-sort-function + (and (fboundp 'vertico-sort-history-length-alpha) 'vertico-sort-history-length-alpha) + "Default sorting function, used if no `display-sort-function' is specified." + :type '(choice + (const :tag "No sorting" nil) + (const :tag "By history, length and alpha" vertico-sort-history-length-alpha) + (const :tag "By history and alpha" vertico-sort-history-alpha) + (const :tag "By length and alpha" vertico-sort-length-alpha) + (const :tag "Alphabetically" vertico-sort-alpha) + (function :tag "Custom function"))) + +(defcustom vertico-sort-override-function nil + "Override sort function which overrides the `display-sort-function'." + :type '(choice (const nil) function)) + +(defgroup vertico-faces nil + "Faces used by Vertico." + :group 'vertico + :group 'faces) + +(defface vertico-multiline '((t :inherit shadow)) + "Face used to highlight multiline replacement characters.") + +(defface vertico-group-title '((t :inherit shadow :slant italic)) + "Face used for the title text of the candidate group headlines.") + +(defface vertico-group-separator '((t :inherit vertico-group-title :strike-through t)) + "Face used for the separator lines of the candidate groups.") + +(defface vertico-current '((t :inherit highlight :extend t)) + "Face used to highlight the currently selected candidate.") + +(defvar-keymap vertico-map + :doc "Vertico minibuffer keymap derived from `minibuffer-local-map'." + :parent minibuffer-local-map + "<remap> <beginning-of-buffer>" #'vertico-first + "<remap> <minibuffer-beginning-of-buffer>" #'vertico-first + "<remap> <end-of-buffer>" #'vertico-last + "<remap> <scroll-down-command>" #'vertico-scroll-down + "<remap> <scroll-up-command>" #'vertico-scroll-up + "<remap> <next-line>" #'vertico-next + "<remap> <previous-line>" #'vertico-previous + "<remap> <next-line-or-history-element>" #'vertico-next + "<remap> <previous-line-or-history-element>" #'vertico-previous + "<remap> <backward-paragraph>" #'vertico-previous-group + "<remap> <forward-paragraph>" #'vertico-next-group + "<remap> <exit-minibuffer>" #'vertico-exit + "<remap> <kill-ring-save>" #'vertico-save + "M-RET" #'vertico-exit-input + "TAB" #'vertico-insert + "<touchscreen-begin>" #'ignore) + +(defvar vertico--locals + '((scroll-margin . 0) + (completion-auto-help . nil) + (pixel-scroll-precision-mode . nil)) + "Vertico minibuffer local variables.") + +(defvar-local vertico--hilit #'identity + "Lazy candidate highlighting function.") + +(defvar-local vertico--candidates-ov nil + "Overlay showing the candidates.") + +(defvar-local vertico--count-ov nil + "Overlay showing the number of candidates.") + +(defvar-local vertico--index -1 + "Index of current candidate or negative for prompt selection.") + +(defvar-local vertico--scroll 0 + "Scroll position.") + +(defvar-local vertico--input nil + "Cons of last minibuffer contents and point or t.") + +(defvar-local vertico--candidates nil + "List of candidates.") + +(defvar-local vertico--metadata nil + "Completion metadata.") + +(defvar-local vertico--base "" + "Base string, which is concatenated with the candidate.") + +(defvar-local vertico--total 0 + "Length of the candidate list `vertico--candidates'.") + +(defvar-local vertico--lock-candidate nil + "Lock-in current candidate.") + +(defvar-local vertico--lock-groups nil + "Lock-in current group order.") + +(defvar-local vertico--groups nil + "List of current group titles.") + +(defvar-local vertico--allow-prompt nil + "Prompt selection is allowed.") + +(defun vertico--affixate (cands) + "Annotate CANDS with annotation function." + (if-let* ((aff (vertico--metadata-get 'affixation-function))) + (funcall aff cands) + (if-let* ((ann (vertico--metadata-get 'annotation-function))) + (cl-loop for cand in cands collect + (let ((suff (or (funcall ann cand) ""))) + ;; The default completion UI adds the `completions-annotations' + ;; face if no other faces are present. + (unless (text-property-not-all 0 (length suff) 'face nil suff) + (setq suff (propertize suff 'face 'completions-annotations))) + (list cand "" suff))) + (cl-loop for cand in cands collect (list cand "" ""))))) + +(defun vertico--move-to-front (elem list) + "Move ELEM to front of LIST." + (if-let* ((found (member elem list))) ;; No duplicates, compare with Corfu. + (nconc (list (car found)) (delq (setcar found nil) list)) + list)) + +(defun vertico--filter-completions (&rest args) + "Compute all completions for ARGS with lazy highlighting." + (dlet ((completion-lazy-hilit t) (completion-lazy-hilit-fn nil)) + (static-if (>= emacs-major-version 30) + (cons (apply #'completion-all-completions args) completion-lazy-hilit-fn) + (cl-letf* ((orig-pcm (symbol-function #'completion-pcm--hilit-commonality)) + (orig-flex (symbol-function #'completion-flex-all-completions)) + ((symbol-function #'completion-flex-all-completions) + (lambda (&rest args) + ;; Unfortunately for flex we have to undo the lazy highlighting, since flex uses + ;; the completion-score for sorting, which is applied during highlighting. + (cl-letf (((symbol-function #'completion-pcm--hilit-commonality) orig-pcm)) + (apply orig-flex args)))) + ((symbol-function #'completion-pcm--hilit-commonality) + (lambda (pattern cands) + (setq completion-lazy-hilit-fn + (lambda (x) + ;; `completion-pcm--hilit-commonality' sometimes throws an internal error + ;; for example when entering "/sudo:://u". + (condition-case nil + (car (completion-pcm--hilit-commonality pattern (list x))) + (t x)))) + cands)) + ((symbol-function #'completion-hilit-commonality) + (lambda (cands prefix &optional base) + (setq completion-lazy-hilit-fn + (lambda (x) (car (completion-hilit-commonality (list x) prefix base)))) + (and cands (nconc cands base))))) + (cons (apply #'completion-all-completions args) completion-lazy-hilit-fn))))) + +(defun vertico--metadata-get (prop) + "Return PROP from completion metadata." + (compat-call completion-metadata-get vertico--metadata prop)) + +(defun vertico--sort-function () + "Return the sorting function." + (or vertico-sort-override-function + (vertico--metadata-get 'display-sort-function) + vertico-sort-function)) + +(defun vertico--compute (input) + "Compute state given INPUT." + (pcase-let* ((`(,str . ,pt) input) + (table minibuffer-completion-table) + (pred minibuffer-completion-predicate) + (before (substring str 0 pt)) + (after (substring str pt)) + ;; bug#47678: `completion-boundaries' fails for `partial-completion' + ;; if the cursor is moved before the slashes of "~//". + ;; See also corfu.el which has the same issue. + (bounds (condition-case nil + (completion-boundaries before table pred after) + (t (cons 0 (length after))))) + (field (substring str (car bounds) (+ pt (cdr bounds)))) + ;; bug#75910: category instead of `minibuffer-completing-file-name' + (completing-file (eq 'file (vertico--metadata-get 'category))) + (`(,all . ,hl) (vertico--filter-completions str table pred pt vertico--metadata)) + (base (or (when-let* ((z (last all))) (prog1 (cdr z) (setcdr z nil))) 0)) + (vertico--base (substring str 0 base)) + (def (or (car-safe minibuffer-default) minibuffer-default)) + (groups) (def-missing) (lock)) + ;; Filter the ignored file extensions. We cannot use modified predicate for this filtering, + ;; since this breaks the special casing in the `completion-file-name-table' for `file-exists-p' + ;; and `file-directory-p'. + (when completing-file (setq all (completion-pcm--filename-try-filter all))) + ;; Sort using the `display-sort-function' or the Vertico sort functions + (setq all (delete-consecutive-dups (funcall (or (vertico--sort-function) #'identity) all))) + ;; Move special candidates: "field" appears at the top, before "field/", before default value + (when (stringp def) + (setq all (vertico--move-to-front def all))) + (when (and completing-file (not (string-suffix-p "/" field))) + (setq all (vertico--move-to-front (concat field "/") all))) + (setq all (vertico--move-to-front field all)) + (when-let* ((fun (and all (vertico--metadata-get 'group-function)))) + (setq groups (vertico--group-by fun all) all (car groups))) + (setq def-missing (and def (equal str "") (not (member def all))) + lock (and vertico--lock-candidate ;; Locked position of old candidate. + (if (< vertico--index 0) -1 + (seq-position all (nth vertico--index vertico--candidates))))) + `((vertico--input . ,input) + (vertico--base . ,vertico--base) + (vertico--metadata . ,vertico--metadata) + (vertico--candidates . ,all) + (vertico--total . ,(length all)) + (vertico--hilit . ,(or hl #'identity)) + (vertico--allow-prompt . ,(and (not (eq vertico-preselect 'no-prompt)) + (or def-missing (eq vertico-preselect 'prompt) + (memq minibuffer--require-match + '(nil confirm confirm-after-completion))))) + (vertico--lock-candidate . ,lock) + (vertico--groups . ,(cdr groups)) + (vertico--index . ,(or lock + (if (or def-missing (eq vertico-preselect 'prompt) (not all) + (and completing-file (eq vertico-preselect 'directory) + (= (length vertico--base) (length str)) + (test-completion str table pred))) + -1 0)))))) + +(defun vertico--hilit (cand) + "Highlight CAND string with lazy highlighting." + ;; bug#77754: Highlight unquoted string. + (funcall vertico--hilit (substring (or (get-text-property + 0 'completion--unquoted cand) cand)))) + +(defun vertico--cycle (list n) + "Rotate LIST to position N." + (nconc (copy-sequence (nthcdr n list)) (seq-take list n))) + +(defun vertico--group-by (fun elems) + "Group ELEMS by FUN." + (let ((ht (make-hash-table :test #'equal)) titles groups) + ;; Build hash table of groups + (cl-loop for elem on elems + for title = (funcall fun (car elem) nil) do + (if-let* ((group (gethash title ht))) + (setcdr group (setcdr (cdr group) elem)) ;; Append to tail of group + (puthash title (cons elem elem) ht) ;; New group element (head . tail) + (push title titles))) + (setq titles (nreverse titles)) + ;; Cycle groups if `vertico--lock-groups' is set + (when-let* ((group (seq-find (lambda (group) (gethash group ht)) + vertico--lock-groups))) + (setq titles (vertico--cycle titles (seq-position titles group)))) + ;; Build group list + (dolist (title titles) + (push (gethash title ht) groups)) + ;; Unlink last tail + (setcdr (cdar groups) nil) + (setq groups (nreverse groups)) + ;; Link groups + (let ((link groups)) + (while (cdr link) + (setcdr (cdar link) (caadr link)) + (pop link))) + (cons (caar groups) titles))) + +(defun vertico--remote-p (path) + "Return t if PATH is a remote path." + (string-match-p "\\`/[^/|:]+:" (substitute-in-file-name path))) + +(defun vertico--update (&optional interruptible) + "Update state, optionally INTERRUPTIBLE." + (let* ((pt (max 0 (- (point) (minibuffer-prompt-end)))) + (str (minibuffer-contents-no-properties)) + (input (cons str pt))) + (unless (or (and interruptible (input-pending-p)) (equal vertico--input input)) + ;; Redisplay to make input immediately visible before expensive candidate + ;; recomputation (gh:minad/vertico#89). No redisplay during init because + ;; of flicker. + (when (and interruptible (consp vertico--input)) + ;; Prevent recursive exhibit from timer (`consult-vertico--refresh'). + (cl-letf (((symbol-function #'vertico--exhibit) #'ignore)) (redisplay))) + (pcase (let ((vertico--metadata (completion-metadata (substring str 0 pt) + minibuffer-completion-table + minibuffer-completion-predicate))) + ;; If Tramp is used, do not compute the candidates in an + ;; interruptible fashion, since this will break the Tramp + ;; password and user name prompts (See gh:minad/vertico#23). + (if (or (not interruptible) + (and (eq 'file (vertico--metadata-get 'category)) + (or (vertico--remote-p str) (vertico--remote-p default-directory)))) + (vertico--compute input) + (let ((non-essential t)) + (while-no-input (vertico--compute input))))) + ('nil (abort-recursive-edit)) + ((and state (pred consp)) + (dolist (s state) (set (car s) (cdr s)))))))) + +(defun vertico--display-string (str) + "Return display STR without display and invisible properties." + (let ((end (length str)) (pos 0) chunks) + (while (< pos end) + (let ((nextd (next-single-property-change pos 'display str end)) + (disp (get-text-property pos 'display str))) + (if (stringp disp) + (let ((face (get-text-property pos 'face str))) + (when face + (add-face-text-property 0 (length disp) face t (setq disp (concat disp)))) + (setq pos nextd chunks (cons disp chunks))) + (while (< pos nextd) + (let ((nexti (next-single-property-change pos 'invisible str nextd))) + (unless (or (get-text-property pos 'invisible str) + (and (= pos 0) (= nexti end))) ;; full string -> no allocation + (push (substring str pos nexti) chunks)) + (setq pos nexti)))))) + (if chunks (apply #'concat (nreverse chunks)) str))) + +(defun vertico--window-width () + "Return minimum width of windows, which display the minibuffer." + (cl-loop for win in (get-buffer-window-list) minimize (window-width win))) + +(defun vertico--truncate-multiline (str max) + "Truncate multiline STR to MAX." + (let ((pos 0) (res "")) + (while (and (< (length res) (* 2 max)) (string-match "\\(\\S-+\\)\\|\\s-+" str pos)) + (setq res (concat res (if (match-end 1) (match-string 0 str) + (if (string-search "\n" (match-string 0 str)) + (car vertico-multiline) " "))) + pos (match-end 0))) + (truncate-string-to-width (string-trim res) max 0 nil (cdr vertico-multiline)))) + +(defun vertico--compute-scroll () + "Compute new scroll position." + (let ((off (max (min vertico-scroll-margin (/ vertico-count 2)) 0)) + (corr (if (= vertico-scroll-margin (/ vertico-count 2)) (1- (mod vertico-count 2)) 0))) + (setq vertico--scroll (min (max 0 (- vertico--total vertico-count)) + (max 0 (+ vertico--index off 1 (- vertico-count)) + (min (- vertico--index off corr) vertico--scroll)))))) + +(defun vertico--format-group-title (title cand) + "Format group TITLE given the current CAND." + ;; Copy candidate highlighting if title is a prefix of the candidate. + (when (string-prefix-p title cand) + (setq title (substring cand 0 (length title))) + (vertico--remove-face 0 (length title) 'completions-first-difference title)) + (setq title (substring title)) + (add-face-text-property 0 (length title) 'vertico-group-title t title) + (format (concat vertico-group-format "\n") title)) + +(defun vertico--format-count () + "Format the count string." + (format (car vertico-count-format) + (format (cdr vertico-count-format) + (cond ((>= vertico--index 0) (1+ vertico--index)) + (vertico--allow-prompt "*") + (t "!")) + vertico--total))) + +(defun vertico--display-count () + "Update count overlay `vertico--count-ov'." + (move-overlay vertico--count-ov (point-min) (point-min)) + (overlay-put vertico--count-ov 'before-string + (if vertico-count-format (vertico--format-count) ""))) + +(defun vertico--prompt-selection () + "Highlight the prompt if selected." + (let ((inhibit-modification-hooks t)) + (if (and (< vertico--index 0) vertico--allow-prompt) + (add-face-text-property (minibuffer-prompt-end) (point-max) 'vertico-current 'append) + (vertico--remove-face (minibuffer-prompt-end) (point-max) 'vertico-current)))) + +(defun vertico--remove-face (beg end face &optional obj) + "Remove FACE between BEG and END from OBJ." + (while (< beg end) + (let ((next (next-single-property-change beg 'face obj end))) + (when-let* ((val (get-text-property beg 'face obj))) + (put-text-property beg next 'face (remq face (ensure-list val)) obj)) + (setq beg next)))) + +(defun vertico--debug (&rest _) + "Debugger used by `vertico--protect'." + (let ((inhibit-message t)) + (require 'backtrace) + (declare-function backtrace-to-string "backtrace") + (message "Vertico detected an error:\n%s" (backtrace-to-string))) + (let (message-log-max) + (message "%s %s" + (propertize "Vertico detected an error:" 'face 'error) + (substitute-command-keys "Press \\[view-echo-area-messages] to see the stack trace"))) + nil) + +(defun vertico--protect (fun) + "Protect FUN such that errors are caught. +If an error occurs, the FUN is retried with `debug-on-error' enabled and +the stack trace is shown in the *Messages* buffer." + (static-if (fboundp 'handler-bind) ;; Available on Emacs 30 + (ignore-errors + (handler-bind ((error #'vertico--debug)) + (funcall fun))) + (when (or debug-on-error (condition-case nil + (progn (funcall fun) nil) + (error t))) + (let ((debug-on-error t) + (debugger #'vertico--debug)) + (condition-case nil + (funcall fun) + ((debug error) nil)))))) + +(defun vertico--exhibit () + "Exhibit completion UI." + (vertico--protect + (lambda () + (let ((buffer-undo-list t)) ;; Overlays affect point position and undo list! + (vertico--update 'interruptible) + (vertico--prompt-selection) + (vertico--display-count) + (vertico--display-candidates (vertico--arrange-candidates)))))) + +(defun vertico--goto (index) + "Go to candidate with INDEX." + (setq vertico--index + (max (if (or vertico--allow-prompt (= 0 vertico--total)) -1 0) + (min index (1- vertico--total))) + vertico--lock-candidate (or (>= vertico--index 0) vertico--allow-prompt))) + +(defun vertico--candidate (&optional hl) + "Return current candidate string with optional highlighting if HL is non-nil." + (let ((content (or (car-safe vertico--input) (minibuffer-contents-no-properties)))) + (cond + ((>= vertico--index 0) + (let ((cand (substring (nth vertico--index vertico--candidates)))) + ;; XXX Drop the completions-common-part face which is added by the + ;; `completion--twq-all' hack. This should better be fixed in Emacs + ;; itself, the corresponding code is already marked as fixme. + (vertico--remove-face 0 (length cand) 'completions-common-part cand) + (concat vertico--base (if hl (vertico--hilit cand) cand)))) + ((and (equal content "") (or (car-safe minibuffer-default) minibuffer-default))) + (t content)))) + +(defun vertico--match-p (input) + "Return t if INPUT is a valid match." + (let ((rm minibuffer--require-match)) + (or (memq rm '(nil confirm-after-completion)) + (equal "" input) ;; Null completion, returns default value + (if (functionp rm) (funcall rm input) ;; require-match can be a function + (test-completion input minibuffer-completion-table minibuffer-completion-predicate)) + (if (eq rm 'confirm) (eq (ignore-errors (read-char "Confirm")) 13) + (minibuffer-message "Match required") nil)))) + +(cl-defgeneric vertico--format-candidate (cand prefix suffix index _start) + "Format CAND given PREFIX, SUFFIX and INDEX." + (setq cand (vertico--display-string (concat prefix cand suffix "\n"))) + (when (= index vertico--index) + (add-face-text-property 0 (length cand) 'vertico-current 'append cand)) + cand) + +(cl-defgeneric vertico--arrange-candidates () + "Arrange candidates." + (vertico--compute-scroll) + (let ((curr-line 0) lines) + ;; Compute group titles + (let* (title (index vertico--scroll) + (group-fun (and vertico-group-format (vertico--metadata-get 'group-function))) + (candidates + (vertico--affixate + (cl-loop repeat vertico-count for c in (nthcdr index vertico--candidates) + collect (vertico--hilit c))))) + (pcase-dolist ((and cand `(,str . ,_)) candidates) + (when-let* ((new-title (and group-fun (funcall group-fun str nil)))) + (unless (equal title new-title) + (setq title new-title) + (push (vertico--format-group-title title str) lines)) + (setcar cand (funcall group-fun str 'transform))) + (when (= index vertico--index) + (setq curr-line (length lines))) + (push (cons index cand) lines) + (cl-incf index))) + ;; Drop excess lines + (setq lines (nreverse lines)) + (cl-loop for count from (length lines) above vertico-count do + (if (< curr-line (/ count 2)) + (nbutlast lines) + (setq curr-line (1- curr-line) lines (cdr lines)))) + ;; Format candidates + (let ((max-width (- (vertico--window-width) 4)) start) + (cl-loop for line on lines do + (pcase (car line) + (`(,index ,cand ,prefix ,suffix) + (setq start (or start index)) + (when (string-search "\n" cand) + (setq cand (vertico--truncate-multiline cand max-width))) + (setcar line (vertico--format-candidate cand prefix suffix index start)))))) + lines)) + +(cl-defgeneric vertico--display-candidates (lines) + "Update candidates overlay `vertico--candidates-ov' with LINES." + (move-overlay vertico--candidates-ov (point-max) (point-max)) + (overlay-put vertico--candidates-ov 'before-string + (apply #'concat #(" " 0 1 (cursor t)) (and lines "\n") lines)) + (vertico--resize-window (length lines))) + +(cl-defgeneric vertico--resize-window (height) + "Resize active minibuffer window to HEIGHT." + (setq-local truncate-lines (< (point) (* 0.8 (vertico--window-width))) + resize-mini-windows 'grow-only + max-mini-window-height 1.0) + (unless truncate-lines (set-window-hscroll nil 0)) + (unless (frame-root-window-p (active-minibuffer-window)) + (unless vertico-resize (setq height (max height vertico-count))) + (let ((dp (- (max (cdr (window-text-pixel-size)) + (* (default-line-height) (1+ height))) + (window-pixel-height)))) + (when (or (and (> dp 0) (/= height 0)) + (and (< dp 0) (eq vertico-resize t))) + (window-resize nil dp nil nil 'pixelwise))))) + +(cl-defgeneric vertico--prepare () + "Ensure that the state is prepared before running the next command." + (when-let* ((cmd (and (symbolp this-command) (symbol-name this-command))) + ((string-prefix-p "vertico-" cmd)) + ((not (and vertico--metadata (string-prefix-p "vertico-directory-" cmd))))) + (vertico--update))) + +(cl-defgeneric vertico--setup () + "Setup completion UI." + (dolist (var vertico--locals) + (set (make-local-variable (car var)) (cdr var))) + (setq-local vertico--input t + vertico--candidates-ov (make-overlay (point-max) (point-max) nil t t) + vertico--count-ov (make-overlay (point-min) (point-min) nil t t)) + (overlay-put vertico--count-ov 'priority 1) ;; For `minibuffer-depth-indicate-mode' + (use-local-map vertico-map) + (add-hook 'pre-command-hook #'vertico--prepare nil 'local) + (add-hook 'post-command-hook #'vertico--exhibit nil 'local)) + +(cl-defgeneric vertico--advice (&rest app) + "Advice for completion function, apply APP." + (dlet ((completion-eager-display nil)) ;; Available on Emacs 31 + (minibuffer-with-setup-hook #'vertico--setup (apply app)))) + +(defun vertico-first () + "Go to first candidate, or to the prompt when the first candidate is selected." + (interactive) + (vertico--goto (if (> vertico--index 0) 0 -1))) + +(defun vertico-last () + "Go to last candidate." + (interactive) + (vertico--goto (1- vertico--total))) + +(defun vertico-scroll-down (&optional n) + "Go back by N pages." + (interactive "p") + (vertico--goto (max 0 (- vertico--index (* (or n 1) vertico-count))))) + +(defun vertico-scroll-up (&optional n) + "Go forward by N pages." + (interactive "p") + (vertico-scroll-down (- (or n 1)))) + +(defun vertico-next (&optional n) + "Go forward N candidates." + (interactive "p") + (let ((index (+ vertico--index (or n 1)))) + (vertico--goto + (cond + ((not vertico-cycle) index) + ((= vertico--total 0) -1) + (vertico--allow-prompt (1- (mod (1+ index) (1+ vertico--total)))) + (t (mod index vertico--total)))))) + +(defun vertico-previous (&optional n) + "Go backward N candidates." + (interactive "p") + (vertico-next (- (or n 1)))) + +(defun vertico-exit (&optional arg) + "Exit minibuffer with current candidate or input if prefix ARG is given." + (interactive "P") + (when (and (not arg) (>= vertico--index 0)) + (vertico-insert)) + (when (vertico--match-p (minibuffer-contents-no-properties)) + (exit-minibuffer))) + +(defun vertico-next-group (&optional n) + "Cycle N groups forward. +When the prefix argument is 0, the group order is reset." + (interactive "p") + (when (cdr vertico--groups) + (setq vertico--groups (and (not (eq n 0)) + (vertico--cycle vertico--groups + (let ((len (length vertico--groups))) + (- len (mod (- (or n 1)) len))))) + vertico--lock-groups vertico--groups + vertico--lock-candidate nil + vertico--input nil))) + +(defun vertico-previous-group (&optional n) + "Cycle N groups backward. +When the prefix argument is 0, the group order is reset." + (interactive "p") + (vertico-next-group (- (or n 1)))) + +(defun vertico-exit-input () + "Exit minibuffer with input." + (interactive) + (vertico-exit t)) + +(defun vertico-save () + "Save current candidate to kill ring." + (interactive) + (if (or (use-region-p) (not transient-mark-mode)) + (call-interactively #'kill-ring-save) + (kill-new (substring-no-properties (vertico--candidate))))) + +(defun vertico-insert () + "Insert current candidate in minibuffer." + (interactive) + ;; XXX There is a small bug here, depending on interpretation. When completing + ;; "~/emacs/master/li|/calc" where "|" is the cursor, then the returned + ;; candidate only includes the prefix "~/emacs/master/lisp/", but not the + ;; suffix "/calc". Default completion has the same problem when selecting in + ;; the *Completions* buffer. See bug#48356. + (when (> vertico--total 0) + (let ((vertico--index (max 0 vertico--index))) + (insert (prog1 (vertico--candidate) (delete-minibuffer-contents)))))) + +;;;###autoload +(define-minor-mode vertico-mode + "VERTical Interactive COmpletion." + :global t :group 'vertico + (dolist (fun '(completing-read-default completing-read-multiple)) + (if vertico-mode + (advice-add fun :around #'vertico--advice) + (advice-remove fun #'vertico--advice)))) + +(defun vertico--command-p (_sym buffer) + "Return non-nil if Vertico is active in BUFFER." + (buffer-local-value 'vertico--input buffer)) + +;; Do not show Vertico commands in M-X +(dolist (sym '( vertico-next vertico-next-group vertico-previous vertico-previous-group + vertico-scroll-down vertico-scroll-up vertico-exit vertico-insert + vertico-exit-input vertico-save vertico-first vertico-last + vertico-repeat-next ;; autoloads in vertico-repeat.el + vertico-quick-jump vertico-quick-exit vertico-quick-insert ;; autoloads in vertico-quick.el + vertico-directory-up vertico-directory-enter ;; autoloads in vertico-directory.el + vertico-directory-delete-char vertico-directory-delete-word)) + (put sym 'completion-predicate #'vertico--command-p)) + +(provide 'vertico) +;;; vertico.el ends here |
