aboutsummaryrefslogtreecommitdiff
path: root/.config/emacs/lisp/minadstack/marginalia.el
diff options
context:
space:
mode:
Diffstat (limited to '.config/emacs/lisp/minadstack/marginalia.el')
-rw-r--r--.config/emacs/lisp/minadstack/marginalia.el1461
1 files changed, 1461 insertions, 0 deletions
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