[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/vertico 9355d05 01/48: minicomp - initial commit
From: |
Stefan Monnier |
Subject: |
[elpa] externals/vertico 9355d05 01/48: minicomp - initial commit |
Date: |
Mon, 5 Apr 2021 10:54:39 -0400 (EDT) |
branch: externals/vertico
commit 9355d050d45cd8071c784225ce1d53ed567c7617
Author: Daniel Mendler <mail@daniel-mendler.de>
Commit: Daniel Mendler <mail@daniel-mendler.de>
minicomp - initial commit
---
minicomp.el | 418 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 418 insertions(+)
diff --git a/minicomp.el b/minicomp.el
new file mode 100644
index 0000000..5d4d789
--- /dev/null
+++ b/minicomp.el
@@ -0,0 +1,418 @@
+;;; minicomp.el --- Minimal completion system -*- lexical-binding: t -*-
+
+;; Author: Daniel Mendler
+;; Maintainer: Daniel Mendler
+;; Created: 2021
+;; License: GPL-3.0-or-later
+;; Version: 0.1
+;; Package-Requires: ((emacs "27"))
+;; Homepage: https://github.com/minad/minicomp
+
+;; This file is not 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Minimal completion system.
+
+;;; Code:
+
+(eval-when-compile
+ (require 'subr-x))
+
+(defgroup minicomp nil
+ "Minimal completion system."
+ :group 'convenience
+ :prefix "minicomp-")
+
+(defcustom minicomp-sort-threshold 10000
+ "Candidates will only be sorted if there are fewer than this threshold."
+ :type 'integer)
+
+(defcustom minicomp-group-format
+ (concat
+ #(" " 0 4 (face minicomp-group-separator))
+ #(" %s " 0 4 (face minicomp-group-title))
+ #(" " 0 1 (face minicomp-group-separator display (space :align-to right))))
+ "Format string used for the group title."
+ :type '(choice (const nil) string))
+
+(defcustom minicomp-count 10
+ "Maximal number of candidates to show."
+ :type 'integer)
+
+(defgroup minicomp-faces nil
+ "Faces used by Minicomp."
+ :group 'minicomp
+ :group 'faces)
+
+(defface minicomp-group-title
+ '((t :inherit shadow :slant italic))
+ "Face used for the title text of the candidate group headlines.")
+
+(defface minicomp-group-separator
+ '((t :inherit shadow :strike-through t))
+ "Face used for the separator lines of the candidate groups.")
+
+(defface minicomp-current
+ '((t :inherit highlight :extend t))
+ "Face used to highlight the currently selected candidate.")
+
+(defvar minicomp-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map minibuffer-local-map)
+ (define-key map [remap beginning-of-buffer] #'minicomp-beginning-of-buffer)
+ (define-key map [remap minibuffer-beginning-of-buffer]
#'minicomp-beginning-of-buffer)
+ (define-key map [remap end-of-buffer] #'minicomp-end-of-buffer)
+ (define-key map [remap scroll-down-command] #'minicomp-scroll-down)
+ (define-key map [remap scroll-up-command] #'minicomp-scroll-up)
+ (define-key map [remap next-line-or-history-element] #'minicomp-next)
+ (define-key map [remap previous-line-or-history-element]
#'minicomp-previous)
+ (define-key map [remap exit-minibuffer] #'minicomp-exit)
+ (define-key map [remap kill-ring-save] #'minicomp-save)
+ (define-key map "\t" #'minicomp-insert)
+ map)
+ "Minibuffer keymap.")
+
+(defvar-local minicomp--candidates-ov nil
+ "Overlay showing the candidates.")
+
+(defvar-local minicomp--count-ov nil
+ "Overlay showing the number of candidates.")
+
+(defvar-local minicomp--index 0
+ "Index of current candidate or negative for prompt selection.")
+
+(defvar-local minicomp--input nil
+ "Current input string or t.")
+
+(defvar-local minicomp--candidates nil
+ "List of candidates.")
+
+(defvar-local minicomp--base 0
+ "Size of the base string, which is concatenated with the candidate.")
+
+(defvar-local minicomp--total 0
+ "Length of the candidate list `minicomp--candidates'.")
+
+(defvar-local minicomp--keep nil
+ "Keep current candidate index `minicomp--index'.")
+
+(defun minicomp--sort (candidates)
+ "Sort CANDIDATES by history position, length and alphabetically."
+ ;; History disabled if `minibuffer-history-variable' eq `t'.
+ (let* ((list (and (not (eq minibuffer-history-variable t))
+ (symbol-value minibuffer-history-variable)))
+ (hist-len (length list))
+ (hist (make-hash-table :test #'equal
+ :size hist-len))
+ (hist-idx 0)
+ (cand candidates))
+ ;; Store the history position first in a hashtable in order to
+ ;; allow O(1) history lookup.
+ (dolist (elem list)
+ (unless (gethash elem hist)
+ (puthash elem hist-idx hist))
+ (setq hist-idx (1+ hist-idx)))
+ ;; Decorate each candidate with (hist-idx<<13) + length. This
+ ;; way we sort first by hist-idx and then by length. We assume
+ ;; that the candidates are not longer than 2**13 characters.
+ (while cand
+ (setcar cand (cons (car cand)
+ (+ (lsh (gethash (car cand) hist hist-len) 13)
+ (length (car cand)))))
+ (setq cand (cdr cand)))
+ (setq candidates
+ (sort candidates
+ (lambda (c1 c2)
+ (or (< (cdr c1) (cdr c2))
+ (and (= (cdr c1) (cdr c2))
+ (string< (car c1) (car c2))))))
+ cand candidates)
+ ;; Drop decoration from the candidates
+ (while cand
+ (setcar cand (caar cand))
+ (setq cand (cdr cand))))
+ candidates)
+
+(defun minicomp--annotate (metadata candidates)
+ "Annotate CANDIDATES with annotation function specified by METADATA."
+ (let ((aff (or (completion-metadata-get metadata 'affixation-function)
+ (plist-get completion-extra-properties :affixation-function)))
+ (ann (or (completion-metadata-get metadata 'annotation-function)
+ (plist-get completion-extra-properties
:annotation-function))))
+ (cond
+ (aff (funcall aff candidates))
+ (ann (mapcar (lambda (cand) (list cand (or (funcall ann cand) "")))
candidates))
+ (t candidates))))
+
+(defun minicomp--candidates (input metadata)
+ "Recompute candidates with INPUT string and METADATA."
+ (let* ((all (completion-all-completions
+ input
+ minibuffer-completion-table
+ minibuffer-completion-predicate
+ (- (point) (minibuffer-prompt-end))
+ metadata))
+ (base (if-let (last (last all))
+ (prog1 (cdr last)
+ (setcdr last nil))
+ 0))
+ (total (length all))
+ (candidates (if (> total minicomp-sort-threshold)
+ all
+ (funcall
+ (or (completion-metadata-get metadata
'display-sort-function)
+ #'minicomp--sort)
+ all))))
+ (when-let* ((def (cond
+ ((stringp (car-safe minibuffer-default)) (car
minibuffer-default))
+ ((stringp minibuffer-default) minibuffer-default)))
+ (rest (member def candidates)))
+ (setq candidates (nconc (list (car rest)) (delete def candidates))))
+ (when-let (group (completion-metadata-get metadata 'x-group-function))
+ (setq candidates (mapcan #'cdr (funcall group candidates))))
+ (list base total candidates)))
+
+(defun minicomp--recompute (input metadata)
+ "Preprocess candidates with INPUT string and METADATA."
+ (pcase (while-no-input (minicomp--candidates input metadata))
+ (`(,base ,total ,candidates)
+ (if-let* ((old (and candidates
+ minicomp--keep
+ (>= minicomp--index 0)
+ (nth minicomp--index minicomp--candidates)))
+ (idx (seq-position candidates old)))
+ (setq minicomp--index idx)
+ (setq minicomp--keep nil
+ minicomp--index (if candidates 0 -1)))
+ (setq minicomp--base base
+ minicomp--input input
+ minicomp--total total
+ minicomp--candidates candidates))))
+
+(defun minicomp--replace-prop (prop fun str)
+ "Replace STR parts with PROP using FUN."
+ (let ((len (length str)) (pos 0) (chunks))
+ (while (not (= pos len))
+ (let ((end (next-single-property-change pos prop str len)))
+ (push (if-let (val (get-text-property pos prop str))
+ (funcall fun val)
+ (substring str pos end))
+ chunks)
+ (setq pos end)))
+ (apply #'concat (nreverse chunks))))
+
+(defun minicomp--display (input metadata)
+ "Display current candidates with INPUT string and METADATA."
+ (let* ((index (min (max 0 (- minicomp--index (/ minicomp-count 2)))
+ (max 0 (- minicomp--total minicomp-count))))
+ (candidates (seq-take (nthcdr index minicomp--candidates)
minicomp-count))
+ (hl-candidates
+ (if (and (memq 'orderless completion-styles)
+ (fboundp 'orderless-highlight-matches))
+ (orderless-highlight-matches input candidates)
+ candidates))
+ (ann-candidates (minicomp--annotate metadata candidates))
+ (title nil)
+ (displayed (concat " " (and hl-candidates "\n")))
+ (group (completion-metadata-get metadata 'x-group-function)))
+ (dolist (cand hl-candidates)
+ (when-let (new-title (and minicomp-group-format group (caar (funcall
group (list cand)))))
+ (unless (equal title new-title)
+ (setq displayed (concat displayed (format minicomp-group-format
new-title) "\n")
+ title new-title)))
+ (setq cand (thread-last cand
+ (replace-regexp-in-string "[\t ]+" " ")
+ (replace-regexp-in-string "\n+" "⤶")
+ (string-trim)
+ (minicomp--replace-prop 'display (lambda (x) (if (stringp
x) x "")))
+ (minicomp--replace-prop 'invisible (lambda (_) ""))))
+ (pcase-let ((`(,prefix . ,suffix) (pcase (car ann-candidates)
+ (`(,_ ,y) (cons nil y))
+ (`(,x ,_ ,y) (cons x y))
+ (_ (cons nil "")))))
+ (setq cand (concat prefix cand
+ (if (text-property-not-all 0 (length suffix) 'face
nil suffix)
+ suffix
+ (propertize suffix 'face
'completions-annotations)))))
+ (when (= index minicomp--index)
+ (setq cand (concat cand))
+ (add-face-text-property 0 (length cand) 'minicomp-current 'append
cand))
+ (setq displayed (concat displayed cand
+ (when (cdr ann-candidates)
+ (if (= index minicomp--index)
+ (propertize "\n" 'face 'minicomp-current)
+ "\n")))
+ ann-candidates (cdr ann-candidates)
+ index (1+ index)))
+ (put-text-property 0 1 'cursor t displayed)
+ (if (< minicomp--index 0)
+ (add-text-properties (minibuffer-prompt-end) (point-max) '(face
minicomp-current))
+ (remove-text-properties (minibuffer-prompt-end) (point-max) '(face nil)))
+ (move-overlay minicomp--count-ov (point-min) (point-min))
+ (move-overlay minicomp--candidates-ov (point-max) (point-max))
+ (overlay-put minicomp--candidates-ov 'after-string displayed)
+ (overlay-put minicomp--count-ov 'before-string
+ (format "%-6s " (format "%s/%s"
+ (if (< minicomp--index 0) "*"
minicomp--index)
+ minicomp--total)))))
+
+(defun minicomp--exhibit ()
+ "Exhibit completion UI."
+ (setq minicomp--keep (or minicomp--keep (> minicomp--index 0)))
+ (let ((metadata (completion--field-metadata (minibuffer-prompt-end)))
+ (input (minibuffer-contents-no-properties)))
+ (unless (equal minicomp--input input)
+ (minicomp--recompute input metadata))
+ (minicomp--display input metadata)))
+
+(defun minicomp-beginning-of-buffer ()
+ "Go to first candidate."
+ (interactive)
+ (setq minicomp--index (if (> minicomp--total 0) 0 -1)))
+
+(defun minicomp-end-of-buffer ()
+ "Go to last candidate."
+ (interactive)
+ (setq minicomp--index (- minicomp--total 1)))
+
+(defun minicomp-scroll-down ()
+ "Go back by one page."
+ (interactive)
+ (when (>= minicomp--index 0)
+ (setq minicomp--index (max 0 (- minicomp--index minicomp-count)))))
+
+(defun minicomp-scroll-up ()
+ "Go forward by one page."
+ (interactive)
+ (when (>= minicomp--index 0)
+ (setq minicomp--index (min (- minicomp--total 1) (+ minicomp--index
minicomp-count)))))
+
+(defun minicomp-next ()
+ "Go to next candidate."
+ (interactive)
+ (setq minicomp--index (min (1+ minicomp--index) (- minicomp--total 1))))
+
+(defun minicomp-previous ()
+ "Go to previous candidate."
+ (interactive)
+ (setq minicomp--index (max -1 (- minicomp--index 1))))
+
+(defun minicomp-exit ()
+ "Exit minibuffer with current candidate."
+ (interactive)
+ (minicomp-insert)
+ (cond
+ ((let ((input (minibuffer-contents-no-properties)))
+ (or (not minibuffer--require-match)
+ (eq minibuffer-completion-confirm 'confirm-after-completion)
+ (equal "" input)
+ (test-completion input
+ minibuffer-completion-table
+ minibuffer-completion-predicate)))
+ (exit-minibuffer))
+ ((eq minibuffer-completion-confirm 'confirm)
+ (when (eq (read-char "Confirm") 13)
+ (exit-minibuffer)))
+ (t (message "Match required"))))
+
+(defun minicomp-save ()
+ "Save current candidate to kill ring."
+ (interactive)
+ (if (or (use-region-p) (not transient-mark-mode))
+ (call-interactively #'kill-ring-save)
+ (kill-new (minicomp--candidate))))
+
+(defun minicomp-insert ()
+ "Insert current candidate in minibuffer."
+ (interactive)
+ (let ((cand (minicomp--candidate)))
+ (delete-minibuffer-contents)
+ (insert cand)))
+
+(defun minicomp--candidate ()
+ "Return current candidate string."
+ (let ((content (minibuffer-contents-no-properties)))
+ (if (< minicomp--index 0)
+ content
+ (concat (substring content 0 minicomp--base)
+ (nth minicomp--index minicomp--candidates)))))
+
+(defun minicomp--setup ()
+ "Setup completion system."
+ (setq-local max-mini-window-height 1.0)
+ (when (boundp 'orderless-skip-highlighting)
+ (setq-local orderless-skip-highlighting t))
+ ;;(setq-local truncate-lines t)
+ (setq minicomp--input t
+ minicomp--candidates-ov (make-overlay (point-max) (point-max))
+ minicomp--count-ov (make-overlay (point-min) (point-min)))
+ (use-local-map minicomp-map)
+ (add-hook 'post-command-hook #'minicomp--exhibit -99 'local))
+
+(defun minicomp--advice (orig &rest args)
+ "Advice for ORIG completion function, receiving ARGS."
+ (minibuffer-with-setup-hook #'minicomp--setup (apply orig args)))
+
+;;;###autoload
+(define-minor-mode minicomp-mode
+ "Minimal completion system."
+ :global t
+ (if minicomp-mode
+ (progn
+ (advice-add #'completing-read-default :around #'minicomp--advice)
+ (advice-add #'completing-read-multiple :around #'minicomp--advice))
+ (advice-remove #'completing-read-default #'minicomp--advice)
+ (advice-remove #'completing-read-multiple #'minicomp--advice)))
+
+(defun minicomp--consult-candidate ()
+ "Current candidate."
+ (when minicomp--input
+ (minicomp--candidate)))
+
+(defun minicomp--consult-refresh ()
+ "Refresh ui."
+ (when minicomp--input
+ (setq minicomp--input t)
+ (minicomp--exhibit)))
+
+(defun minicomp--embark-target ()
+ "Return embark target."
+ (when minicomp--input
+ (cons (completion-metadata-get (completion--field-metadata
+ (minibuffer-prompt-end))
+ 'category)
+ (minicomp--candidate))))
+
+(defun minicomp--embark-candidates ()
+ "Return embark candidates."
+ (when minicomp--input
+ (cons (completion-metadata-get (completion--field-metadata
+ (minibuffer-prompt-end))
+ 'category)
+ ;; full candidates?
+ minicomp--candidates)))
+
+(with-eval-after-load 'consult
+ (add-hook 'consult--completion-candidate-hook #'minicomp--consult-candidate)
+ (add-hook 'consult--completion-refresh-hook #'minicomp--consult-refresh))
+
+(with-eval-after-load 'embark
+ (add-hook 'embark-target-finders #'minicomp--embark-target)
+ (add-hook 'embark-candidate-collectors #'minicomp--embark-candidates))
+
+(provide 'minicomp)
+;;; minicomp.el ends here
- [elpa] branch externals/vertico created (now 1a9b1b2), Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 3257082 07/48: always keep index when explicitly navigating, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico e2472d1 13/48: simplify, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 9355d05 01/48: minicomp - initial commit,
Stefan Monnier <=
- [elpa] externals/vertico 933e938 06/48: Compute history hash table only once, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico f132f1b 10/48: improve prompt selection, add minicomp--goto, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 9b81d8d 08/48: remove ./ and ../ from file completion candidates, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 5bbddb0 03/48: add while-no-input-ignore-events, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 4ce7c8d 11/48: use seq-subseq, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico f558f53 04/48: extract minicomp--pred, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 9db7c78 20/48: add minicomp-count-format, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 245944a 21/48: remap previous/next-line, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico 7fbaeaf 15/48: Add sorting by history for files, Stefan Monnier, 2021/04/05
- [elpa] externals/vertico c2bfea2 17/48: use candidate returned from affixation function, Stefan Monnier, 2021/04/05