250 changes: 126 additions & 124 deletions consult.el
Expand Up @@ -235,15 +235,25 @@ You may want to add a function which pulses the current line, e.g.,

;;;; Internal variables

(defconst consult--special-char #x100000
"Special character used to encode line prefixes for disambiguation.
We use the first character of the private unicode plane b.")

(defconst consult--special-range #xFFFE
"Special character range.
Size of private unicode plane b.")

(defvar-local consult--narrow nil
"Current narrowing key.")

(defvar-local consult--narrow-prefixes nil
"Narrowing prefixes of the current completion.")

(defvar consult--narrow-separator (concat (string 8203) " ") ;; zero width space
"String used to separate prefix for narrowing.
This string must be made of unique characters,
such that no accidential matching occurs. Therefore
we use a zero-width-space, which generally
does not occur in candidate strings.")
(defvar-local consult--narrow-predicate nil
"Narrowing predicate of the current completion.")

(defvar-local consult--narrow-overlay nil
"Narrowing indicator overlay.")

(defvar consult--gc-threshold 67108864
"Large gc threshold for temporary increase.")
Expand Down Expand Up @@ -302,11 +312,13 @@ KEY is the key function."
(defsubst consult--unique (pos display)
"Generate unique string for POS.
DISPLAY is the string to display instead of the unique string."
(let ((unique-prefix "") (n pos))
(let ((str "") (n pos))
(while (progn
(setq unique-prefix (concat (string (+ #x100000 (% n #xFFFE))) unique-prefix))
(and (>= n #xFFFE) (setq n (/ n #xFFFE)))))
(propertize unique-prefix 'display display)))
(setq str (concat (string (+ consult--special-char
(% n consult--special-range)))
str))
(and (>= n consult--special-range) (setq n (/ n consult--special-range)))))
(propertize str 'display display)))

(defun consult--preview-install (preview fun)
"Install preview support to minibuffer completion.
Expand Down Expand Up @@ -339,53 +351,37 @@ PREVIEW is the preview function."
(consult--preview-install ,preview-var (lambda () ,@body))
,@body))))

(defun consult--narrowed-p (str)
"Return t if STR has some of the narrowing PREFIXES."
(string-match-p (concat "^." (regexp-quote consult--narrow-separator)) str))

(defun consult--narrow-strip (str)
"Strip narrowing prefix from STR."
(if (consult--narrowed-p str)
(replace-regexp-in-string (concat "^." (regexp-quote consult--narrow-separator)) "" str)
str))

(defsubst consult--narrow-candidate (prefix &rest strings)
"Add narrowing prefix PREFIX and concatenate with STRINGS."
(apply #'concat
(propertize (concat (string prefix) consult--narrow-separator) 'display "")
strings))

(defun consult--narrow-indicator (pair)
"Narrowing indicator string for PAIR."
(propertize (concat (string (car pair)) consult--narrow-separator)
'display
(propertize (format "[%s] " (cdr pair))
'face 'consult-narrow-indicator)))
(defun consult--narrow-set (key)
"Set narrowing key `consult--narrow' to KEY."
(when consult--narrow-overlay
(delete-overlay consult--narrow-overlay))
(if (not (setq consult--narrow key))
(setq-local minibuffer-completion-predicate nil)
;; Here we override the completion predicate.
;; We could also combine it with an existing predicate function,
;; if this turns out to be necessary!
(setq-local minibuffer-completion-predicate consult--narrow-predicate)
(setq consult--narrow-overlay (make-overlay (- (minibuffer-prompt-end) 1) (minibuffer-prompt-end)))
(overlay-put consult--narrow-overlay 'before-string
(propertize (format " [%s]" (cdr (assoc key consult--narrow-prefixes)))
'face 'consult-narrow-indicator))))

(defun consult-widen ()
"Widen current completion."
(interactive)
(let ((str (consult--narrow-strip (minibuffer-contents))))
(delete-minibuffer-contents)
(insert str)))
(consult--narrow-set nil))

(defun consult-narrow ()
"Narrow current completion."
(interactive)
(let ((str (consult--narrow-strip (minibuffer-contents))))
(delete-minibuffer-contents)
(insert (concat (consult--narrow-indicator
(assoc last-command-event consult--narrow-prefixes)))
str)))
(consult--narrow-set last-command-event))

(defconst consult--narrow-delete
`(menu-item
"" nil :filter
,(lambda (&optional _)
(let ((str (minibuffer-contents-no-properties)))
(when (and (string-suffix-p consult--narrow-separator str)
(consult--narrowed-p str))
#'delete-minibuffer-contents)))))
(when (string= (minibuffer-contents-no-properties) "")
#'consult-widen))))

(defconst consult--narrow-space
`(menu-item
Expand All @@ -395,7 +391,7 @@ PREVIEW is the preview function."
(when (= 1 (length str))
(when-let (pair (assoc (elt str 0) consult--narrow-prefixes))
(delete-minibuffer-contents)
(insert (consult--narrow-indicator pair))
(consult--narrow-set (car pair))
#'ignore))))))

(defun consult--define-key (map key cmd desc)
Expand All @@ -410,37 +406,38 @@ PREVIEW is the preview function."
(define-key map (vconcat (seq-take key idx) (vector 'which-key (elt key idx)))
`(which-key (,desc . ,cmd)))))

(defun consult--narrow-install (prefixes fun)
(defun consult--narrow-install (predicate prefixes fun)
"Install narrowing in FUN.
PREFIXES is an alist of narrowing prefix strings."
PREDICATE is the narrowing predicate.
PREFIXES is the list of narrowing prefixes."
(minibuffer-with-setup-hook
(:append
(lambda ()
(setq consult--narrow-prefixes prefixes)
(setq consult--narrow-predicate predicate
consult--narrow-prefixes prefixes)
(let ((map (make-composed-keymap nil (current-local-map))))
(when consult-narrow-key
(dolist (pair prefixes)
(consult--define-key
map
(vconcat consult-narrow-key (vector (car pair)))
#'consult-narrow (cdr pair))))
(dolist (pair consult--narrow-prefixes)
(consult--define-key map
(vconcat consult-narrow-key (vector (car pair)))
#'consult-narrow (cdr pair))))
(when consult-widen-key
(consult--define-key map consult-widen-key #'consult-widen "All"))
(define-key map " " consult--narrow-space)
(define-key map [127] consult--narrow-delete)
(use-local-map map))))
(funcall fun)))

(defmacro consult--with-narrow (prefixes &rest body)
(defmacro consult--with-narrow (settings &rest body)
"Setup narrowing in BODY.
PREFIXES is an alist of narrowing prefix strings."
SETTINGS is the narrow settings."
(declare (indent 1))
(let ((prefixes-var (make-symbol "prefixes")))
`(let ((,prefixes-var ,prefixes))
(if ,prefixes-var
(consult--narrow-install ,prefixes-var (lambda () ,@body))
(let ((settings-var (make-symbol "settings")))
`(let ((,settings-var ,settings))
(if ,settings-var
(consult--narrow-install (car ,settings-var) (cdr ,settings-var) (lambda () ,@body))
,@body))))

(defmacro consult--with-increased-gc (&rest body)
Expand Down Expand Up @@ -862,7 +859,9 @@ CAND is the currently selected candidate."
(if (string-blank-p input)
pos
;; Strip unique line number prefix
(while (and (> (length cand) 0) (>= (elt cand 0) #x100000) (< (elt cand 0) #x10FFFE))
(while (and (> (length cand) 0)
(>= (elt cand 0) consult--special-char)
(< (elt cand 0) (+ consult--special-char consult--special-range)))
(setq cand (substring cand 1)))
(let ((start 0)
(end (length cand)))
Expand Down Expand Up @@ -1246,13 +1245,11 @@ for which the command history is used."
"Return list of minor-mode candidate strings."
(mapcar
(pcase-lambda (`(,name . ,sym))
(cons
(consult--narrow-candidate
(if (local-variable-if-set-p sym) ?l ?g)
(consult--narrow-candidate
(if (and (boundp sym) (symbol-value sym)) ?i ?o)
name))
sym))
(list name
sym
(concat
(if (local-variable-if-set-p sym) "l" "g")
(if (and (boundp sym) (symbol-value sym)) "i" "o"))))
(delq nil
(append
;; according to describe-minor-mode-completion-table-for-symbol
Expand All @@ -1272,16 +1269,17 @@ for which the command history is used."
This is an alternative to `minor-mode-menu-from-indicator'."
(interactive)
(call-interactively
(consult--read "Minor mode: "
(consult--minor-mode-candidates)
:require-match t
:category 'minor-mode
:narrow '((?l . "Local")
(?g . "Global")
(?i . "On")
(?o . "Off"))
:lookup #'consult--lookup-candidate
:history 'consult--minor-mode-menu-history)))
(car (consult--read "Minor mode: "
(consult--minor-mode-candidates)
:require-match t
:category 'minor-mode
:narrow '((lambda (cand) (seq-position (caddr cand) consult--narrow))
(?l . "Local")
(?g . "Global")
(?i . "On")
(?o . "Off"))
:lookup #'consult--lookup-candidate
:history 'consult--minor-mode-menu-history))))

;;;###autoload
(defun consult-theme (theme)
Expand Down Expand Up @@ -1317,13 +1315,13 @@ preview if `consult-preview-mode' is enabled."
(enable-theme theme)
(load-theme theme :no-confirm)))))

(defsubst consult--buffer-candidate (prefix cand face)
(defsubst consult--buffer-candidate (type cand face)
"Format virtual buffer candidate.
CAND is the candidate string.
PREFIX is the prefix string for narrowing.
TYPE is the type character.
FACE is the face for the candidate."
(consult--narrow-candidate prefix (propertize cand 'face face)))
(concat (propertize (string (+ consult--special-char type)) 'display "") (propertize cand 'face face)))

(defun consult--buffer (open-buffer open-file open-bookmark)
"Backend implementation of `consult-buffer'.
Expand All @@ -1338,15 +1336,14 @@ Depending on the selected item OPEN-BUFFER, OPEN-FILE or OPEN-BOOKMARK will be u
;; and a regression to the default `switch-to-buffer' implementation.
;; But since invisible buffers are seldom accessed, this is not a big problem.
;; Use `switch-to-buffer' in that case.
(bufs (mapcar
(lambda (x)
(consult--buffer-candidate ?b x 'consult-buffer))
(append
(seq-remove
;; Visible buffers only
(lambda (x) (or (string= x curr-buf) (= (elt x 0) 32)))
(mapcar #'buffer-name (buffer-list)))
(list curr-buf))))
(bufs (mapcar (lambda (x)
(consult--buffer-candidate ?b x 'consult-buffer))
(append
(seq-remove
;; Visible buffers only
(lambda (x) (or (string= x curr-buf) (= (elt x 0) 32)))
(mapcar #'buffer-name (buffer-list)))
(list curr-buf))))
(views (when consult-view-list-function
(mapcar (lambda (x)
(consult--buffer-candidate ?v x 'consult-view))
Expand All @@ -1362,20 +1359,22 @@ Depending on the selected item OPEN-BUFFER, OPEN-FILE or OPEN-BOOKMARK will be u
"Switch to: " (append bufs files views bookmarks)
:history 'consult--buffer-history
:sort nil
:narrow `((?b . "Buffer")
:narrow `((lambda (cand)
(and (not (string= "" cand)) (= (- (elt cand 0) consult--special-char) consult--narrow)))
(?b . "Buffer")
(?f . "File")
(?m . "Bookmark")
,@(when consult-view-list-function '((?v . "View"))))
:category 'virtual-buffer
:lookup
(lambda (_ candidates cand)
(if (member cand candidates)
(cons (pcase (elt cand 0)
(cons (pcase (- (elt cand 0) consult--special-char)
(?b open-buffer)
(?m open-bookmark)
(?v consult-view-open-function)
(?f open-file))
(consult--narrow-strip cand))
(substring cand 1))
;; When candidate is not found in the alist,
;; default to creating a new buffer.
(and (not (string-blank-p cand)) (cons open-buffer cand))))
Expand Down Expand Up @@ -1515,45 +1514,42 @@ Prepend PREFIX in front of all items."
(let ((fns (seq-remove (lambda (x) (listp (cdr x))) items))
(rest (seq-filter (lambda (x) (listp (cdr x))) items)))
(setq items (append rest (list (cons "Functions" fns))))))
;; Narrowing support
(when-let (narrow (consult--imenu-narrow))
(dolist (x items)
(when-let (n (seq-find (lambda (n)
(string-prefix-p (car x) (concat (cdr n) " ")))
narrow))
(setcar x (consult--narrow-candidate (car n) (car x))))))
(seq-sort-by #'car #'string<
(consult--imenu-flatten nil items))))

(defun consult--imenu-narrow ()
"Return narrowing list for imenu."
(cdr (seq-find (lambda (x) (derived-mode-p (car x))) consult-imenu-narrow)))

;;;###autoload
(defun consult-imenu ()
"Choose from flattened `imenu' using `completing-read'."
(interactive)
(imenu
(consult--read
"Go to item: "
(or (consult--imenu-candidates)
(user-error "Imenu is empty"))
:preview (and consult-preview-imenu
(let ((preview (consult--preview-position)))
(lambda (cmd cand state)
(if (eq cmd 'preview)
;; Only handle imenu items which are markers for preview,
;; in order to avoid any bad side effects.
(when (and (consp cand) (markerp (cdr cand)))
(funcall preview cmd (cdr cand) state))
(funcall preview cmd cand state)))))
:require-match t
:narrow (consult--imenu-narrow)
:category 'imenu
:lookup #'consult--lookup-candidate
:history 'consult--imenu-history
:sort nil))
(run-hooks 'consult-after-jump-hook))
(let ((narrow (cdr (seq-find (lambda (x) (derived-mode-p (car x))) consult-imenu-narrow))))
(imenu
(consult--read
"Go to item: "
(or (consult--imenu-candidates)
(user-error "Imenu is empty"))
:preview (and consult-preview-imenu
(let ((preview (consult--preview-position)))
(lambda (cmd cand state)
(if (eq cmd 'preview)
;; Only handle imenu items which are markers for preview,
;; in order to avoid any bad side effects.
(when (and (consp cand) (markerp (cdr cand)))
(funcall preview cmd (cdr cand) state))
(funcall preview cmd cand state)))))
:require-match t
:narrow (cons (lambda (cand)
(when-let (n (cdr (assoc consult--narrow narrow)))
(let* ((c (car cand))
(l (length n)))
(and (> (length c) l)
(eq t (compare-strings n 0 l c 0 l))
(= (elt c l) 32)))))
narrow)
:category 'imenu
:lookup #'consult--lookup-candidate
:history 'consult--imenu-history
:sort nil))
(run-hooks 'consult-after-jump-hook)))

;;;; default completion-system support for preview

Expand Down Expand Up @@ -1597,5 +1593,11 @@ Prepend PREFIX in front of all items."

(add-hook 'consult-preview-mode-hook #'consult--icomplete-preview-setup)

(defun consult--icomplete-refresh (&rest _)
"Refresh icomplete view."
(setq completion-all-sorted-completions nil))

(advice-add #'consult--narrow-set :after #'consult--icomplete-refresh)

(provide 'consult)
;;; consult.el ends here