diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ab6442d..86cbb69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ The format is based on [Keep a Changelog]. ([#313], [#466]). ### Features +* You can quickly select and insert a candidate using the commands + `selectrum-quick-select` and `selectrum-quick-insert` which are + bound to `M-i` and `M-m` (analogue to `C-i` and `C-m` used for + normal insertion and selection). These commands are similar in + functionality to `ivy-avy`. You can configure the used keys via + `selectrum-quick-keys` option and their appearance via + `selectrum-quick-keys-highlight` and `selectrum-quick-keys-match` + face ([#16], [#304], [#479]). * Add support for `x-group-function` completion metadata. The group title formatting is controlled by the customization variable `selectrum-group-format` and the faces `selectrum-group-separator` @@ -41,6 +49,8 @@ packages. Users of `selectrum-prescient` can update to configure candidate and exiting by pressing `RET` no longer fails when there are existing candidates already selected using `TAB` ([#460]). +[#16]: https://github.com/raxod502/selectrum/issues/16 +[#304]: https://github.com/raxod502/selectrum/issues/304 [#313]: https://github.com/raxod502/selectrum/issues/313 [#419]: https://github.com/raxod502/selectrum/issues/419 [#450]: https://github.com/raxod502/selectrum/issues/450 @@ -54,6 +64,7 @@ packages. Users of `selectrum-prescient` can update to configure [#463]: https://github.com/raxod502/selectrum/pull/463 [#465]: https://github.com/raxod502/selectrum/pull/465 [#466]: https://github.com/raxod502/selectrum/pull/466 +[#479]: https://github.com/raxod502/selectrum/pull/479 [#488]: https://github.com/raxod502/selectrum/pull/488 ## 3.1 (released 2021-02-21) diff --git a/README.md b/README.md index edd9b905..8528a67a 100644 --- a/README.md +++ b/README.md @@ -146,24 +146,26 @@ how to fix it. * *To navigate to a candidate:* use the standard motion commands (``, ``, `C-v`, `M-v`, `M-<`, `M->`). If you prefer, you can use `C-p` and `C-n` instead of the arrow keys. -* *To accept the currently selected candidate:* type `RET`. (With a - prefix argument, accept instead the candidate at that point in the - list, counting from one. See `selectrum-show-indices`. The value - zero means to accept exactly what you've typed, as in the next +* *To accept the currently selected candidate:* type `RET`/`C-m`. + (With a prefix argument, accept instead the candidate at that point + in the list, counting from one. See `selectrum-show-indices`. The + value zero means to accept exactly what you've typed, as in the next bullet point.) You can also click the left mouse button on a - candidate to choose it. + candidate to choose it or use `M-m` to select one using + `selectrum-quick-keys`. * *To submit what you've typed, even if it's not a candidate:* you can use `` or `C-p` to select the user input just like a regular candidate, and type `RET` as usual. (Alternatively, you can type `C-j` to submit your exact input without selecting it first.) * *To abort:* as per usual, type `C-g`. * *To navigate into the currently selected directory while finding a - file\:* type `TAB`. (What this actually does is insert the currently - selected candidate into the minibuffer, which for `find-file` has - the effect of navigating into a directory.) With a positive prefix - argument, insert the candidate at that display position (see - `selectrum-show-indices`). You can also right click on a candidate - to insert it into the minibuffer. + file\:* type `TAB`/`C-i`. (What this actually does is insert the + currently selected candidate into the minibuffer, which for + `find-file` has the effect of navigating into a directory.) With a + positive prefix argument, insert the candidate at that display + position (see `selectrum-show-indices`). You can also right click on + a candidate to insert it into the minibuffer or use `M-i` for + inserting one using `selectrum-quick-keys`. * *To copy the current candidate:* type `M-w` or what is bound to `kill-ring-save`. When there's an active region in your input, this still copies the active region. The behavior of `M-w` is not @@ -336,6 +338,13 @@ used refinement function. The built-in `completion-styles` support the `selectrum-completion-in-region-styles`. * The option `selectrum-should-sort` controls whether preprocessing functions should sort. +* You can configure the keys for quick candidate insertion and + selection using `selectrum-quick-keys`. These are used when using + the commands `selectrum-quick-select` or `selectrum-quick-insert` + which provide you an `ivy-avy` like interface to quickly select a + candidate via key annotations. You can configure the appearance of + these key annotations with `selectrum-quick-keys-highlight` and + `selectrum-quick-keys-match` face. ### Complementary extensions diff --git a/selectrum.el b/selectrum.el index 0415001e..6c3f3e40 100644 --- a/selectrum.el +++ b/selectrum.el @@ -100,6 +100,16 @@ list of strings." ;;; Faces +(defface selectrum-quick-keys-highlight + '((t :inherit lazy-highlight)) + "Face used for `selectrum-quick-keys'." + :group 'selectrum-faces) + +(defface selectrum-quick-keys-match + '((t :inherit isearch)) + "Face used for matches of `selectrum-quick-keys'." + :group 'selectrum-faces) + (defface selectrum-group-title '((t :inherit shadow :slant italic)) "Face used for the title text of the candidate group headlines." @@ -150,6 +160,11 @@ parts of the input." "Format string for the default value in the minibuffer." :type '(choice (const nil) string)) +(defcustom selectrum-quick-keys '(?a ?s ?d ?f ?j ?k ?l ?i ?g ?h) + "Keys for quick selection. +Used by `selectrum-quick-select' and `selectrum-quick-insert'." + :type 'character) + (defcustom selectrum-group-format (concat #(" " 0 4 (face selectrum-group-separator)) @@ -521,6 +536,8 @@ function and BODY opens the minibuffer." (define-key map (kbd "C-j") #'selectrum-submit-exact-input) (define-key map (kbd "TAB") #'selectrum-insert-current-candidate) (define-key map (kbd "M-q") 'selectrum-cycle-display-style) + (define-key map (kbd "M-i") 'selectrum-quick-insert) + (define-key map (kbd "M-m") 'selectrum-quick-select) ;; Return the map. map) "Keymap used by Selectrum in the minibuffer.") @@ -533,6 +550,12 @@ at the start of the list.") (defvar selectrum--display-action-buffer " *selectrum*" "Buffer to display candidates using `selectrum-display-action'.") +(defvar selectrum--quick-fun nil + "Function for quick selection. +Used by `selectrum-quick-select' and `selectrum-quick-insert'. +Receives the display index and candidate and should return the +new candidate string used for display.") + (defvar selectrum--crm-separator-alist '((":\\|,\\|\\s-" . ",") ("[ \t]*:[ \t]*" . ":") @@ -1778,6 +1801,10 @@ which is displayed in the UI." (when hl (setq displayed-candidate (selectrum--selection-highlight displayed-candidate))) + (when (and selectrum--quick-fun + (not hl)) + (setq displayed-candidate + (funcall selectrum--quick-fun display-index displayed-candidate))) (when-let (show-indices (cond ((functionp selectrum-show-indices) selectrum-show-indices) @@ -2105,6 +2132,88 @@ Only to be used from `selectrum-select-from-history'" (propertize (selectrum-get-current-candidate 'notfull) 'selectum--insert t))) +(defun selectrum--quick-keys (len keys) + "Get list of key combinations up to key length LEN. +KEYS is a list of key strings to combine." + (unless (zerop len) + (cl-loop with list = keys + with olist = keys + repeat (1- len) + do (setq olist + (cl-loop + for char in list + nconc (cl-loop for ochar in olist + collect (concat char ochar)))) + finally return olist))) + +(defun selectrum--quick-read () + "Read index interactively using `selectrum-quick-keys'." + (unless (cdr selectrum-quick-keys) + (user-error "`selectrum-quick-keys' needs at least two keys")) + (when (< selectrum--actual-num-candidates-displayed 2) + (user-error "No candidates for quick selection")) + (let* ((qkeys (mapcar #'char-to-string selectrum-quick-keys)) + (nkeys (length qkeys)) + (needed selectrum--actual-num-candidates-displayed) + (len (ceiling (log needed nkeys))) + (keys (seq-take (selectrum--quick-keys len qkeys) needed)) + (input nil) + (read-char (lambda () + (let ((char nil)) + (unwind-protect + (when (characterp (setq char (read-char))) + char) + (when (or (eq ?\C-g char) + (not (characterp char))) + (let ((selectrum--quick-fun nil)) + (selectrum--update))))))) + (selectrum--quick-fun + (lambda (i cand) + (let ((str (propertize (or (nth i keys) "") + 'face 'selectrum-quick-keys-highlight))) + (when (and input (string-match (concat "\\`" input) str)) + (setq str (copy-sequence str)) + (add-face-text-property 0 (match-end 0) + 'selectrum-quick-keys-match nil str)) + (concat str (substring cand (min (length cand) + (length str)))))))) + (if-let* ((input + (cl-loop with pressed = 0 + while (< pressed len) + do (selectrum--update) + for char = (funcall read-char) + for key = (when char + (char-to-string char)) + if (and (not (zerop pressed)) + (equal char ?\C-?)) + do (setq pressed (1- pressed) + input (substring + input 0 (1- (length input)))) + else if (not (member key qkeys)) + return nil + else + do (setq pressed (1+ pressed) + input (concat input key)) + finally return input)) + (pos (cl-position input keys :test #'string=))) + (+ selectrum--first-index-displayed pos) + (prog1 nil + (message "No matching key"))))) + +(defun selectrum-quick-select () + "Select a candidate using `selectrum-quick-keys'." + (interactive) + (when-let (index (selectrum--quick-read)) + (let ((selectrum--current-candidate-index index)) + (selectrum-select-current-candidate)))) + +(defun selectrum-quick-insert () + "Insert a candidate using `selectrum-quick-keys'." + (interactive) + (when-let (index (selectrum--quick-read)) + (let ((selectrum--current-candidate-index index)) + (selectrum-insert-current-candidate)))) + ;;; Main entry points (cl-defun selectrum--read