Initial commit of Corfu candidate overlay
The minor mode uses underlying corfu completion data to show the first candidate inline while typing.
This commit is contained in:
commit
cfd74e2f6b
1 changed files with 222 additions and 0 deletions
222
corfu-candidate-overlay.el
Normal file
222
corfu-candidate-overlay.el
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
;;; corfu-candidate-overlay.el --- Show first candidate in an overlay while typing -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2021-2023 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
;; Author: Adam Kruszewski <adam@kruszewski.name>
|
||||||
|
;; Maintainer: Adam Kruszewski <adam@kruszewski.name>
|
||||||
|
;; Created: 2023
|
||||||
|
;; Version: 0.1
|
||||||
|
;; Package-Requires: ((emacs "27.1") (corfu "0.36"))
|
||||||
|
;; Homepage: https://code.bsdgeek.org/adam/corfu-candidate-overlay/
|
||||||
|
|
||||||
|
;; 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:
|
||||||
|
|
||||||
|
;; Show first candidate in an inline overlay while typing.
|
||||||
|
;; When there is only one candidate the overlay text will
|
||||||
|
;; be underlined.
|
||||||
|
;;
|
||||||
|
;; It is ment to be used with `corfu-auto' set to `nil', and
|
||||||
|
;; executing the corfu completion popup with a keybind.
|
||||||
|
;;
|
||||||
|
;; Enable by executing `corfu-candidate-overlay-mode'.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'corfu)
|
||||||
|
|
||||||
|
(defvar-local corfu--candidate-overlay nil
|
||||||
|
"Overlay for Corfu candidates display when typing.")
|
||||||
|
|
||||||
|
(defvar-local corfu--candidate-last-point nil
|
||||||
|
"Last point location when the overlay was calculated for.")
|
||||||
|
|
||||||
|
(defvar corfu--candidate-overlay-map nil
|
||||||
|
"Keymap to dismiss the Corfu candidate overlay.")
|
||||||
|
|
||||||
|
(defcustom corfu-overlay-auto-commands
|
||||||
|
'("delete-backward-char\\'")
|
||||||
|
"Additional commands apart from corfu-auto-commands which initiate
|
||||||
|
completion candidate overlay."
|
||||||
|
:type '(repeat (choice regexp symbol))
|
||||||
|
:group 'corfu)
|
||||||
|
|
||||||
|
(defface corfu--candidate-overlay-face
|
||||||
|
'((((background light))
|
||||||
|
:foreground "MistyRose4")
|
||||||
|
(((background dark))
|
||||||
|
:foreground "wheat"))
|
||||||
|
"Face used for the overlay with the first candidate.")
|
||||||
|
|
||||||
|
(defface corfu--candidate-overlay-face-exact-match
|
||||||
|
'((t (:inherit 'corfu--candidate-overlay-face :underline t)))
|
||||||
|
"Face used for the overlay when there is only one candidate.")
|
||||||
|
|
||||||
|
(defun corfu--candiate-overlay-prepare (beg end)
|
||||||
|
"Sets the default properties of the candidates overlay.
|
||||||
|
The overlay can be dismissed with a mouse click."
|
||||||
|
(when (not corfu--candidate-overlay-map)
|
||||||
|
(setq corfu--candidate-overlay-map (make-sparse-keymap))
|
||||||
|
(define-key corfu--candidate-overlay-map (kbd "<mouse-1>")
|
||||||
|
(lambda ()
|
||||||
|
(interactive)
|
||||||
|
(delete-overlay corfu--candidate-overlay))))
|
||||||
|
|
||||||
|
(if corfu--candidate-overlay
|
||||||
|
(move-overlay corfu--candidate-overlay end end)
|
||||||
|
(progn
|
||||||
|
(setq corfu--candidate-overlay (make-overlay end end nil))
|
||||||
|
;; priority of 1k is the value used by Corfu.
|
||||||
|
(overlay-put corfu--candidate-overlay 'priority 1000))))
|
||||||
|
|
||||||
|
(defun corfu--candidate-overlay-update (beg end prefix candidate how-many-candidates)
|
||||||
|
"Updates the candidate overlay with the first candidate found by Corfu."
|
||||||
|
(corfu--candiate-overlay-prepare beg end)
|
||||||
|
|
||||||
|
(unless (string-empty-p candidate)
|
||||||
|
(add-text-properties 0 1 '(cursor 1) candidate))
|
||||||
|
|
||||||
|
(overlay-put corfu--candidate-overlay 'window (selected-window))
|
||||||
|
(overlay-put corfu--candidate-overlay 'after-string
|
||||||
|
(propertize
|
||||||
|
candidate
|
||||||
|
'face (if (= how-many-candidates 1)
|
||||||
|
'corfu--candidate-overlay-face-exact-match
|
||||||
|
'corfu--candidate-overlay-face)
|
||||||
|
'keymap corfu--candidate-overlay-map)))
|
||||||
|
|
||||||
|
(defun corfu-hide-candidate-overlay ()
|
||||||
|
"Hide the candidate overlay."
|
||||||
|
(when (and
|
||||||
|
corfu--candidate-overlay
|
||||||
|
(overlayp corfu--candidate-overlay))
|
||||||
|
;; 'invisible property doesn't work really; deleting the overlay
|
||||||
|
;; would need to recreate the object on basically each keystroke
|
||||||
|
;; and I don't like the perspective of it, would also flicker
|
||||||
|
;; for sure - so we keep the one overlay and we clear the contents.
|
||||||
|
(overlay-put corfu--candidate-overlay 'after-string "")))
|
||||||
|
|
||||||
|
(defun corfu-show-candidate-overlay ()
|
||||||
|
"Computes completion candidates just like Corfu and updats the candidate
|
||||||
|
overlay to reflect the first one. Uses different face when there is only
|
||||||
|
one candidate available (defaults to underline)."
|
||||||
|
(let ((value (while-no-input ;; Interruptible capf query
|
||||||
|
(run-hook-wrapped 'completion-at-point-functions #'corfu--capf-wrapper))))
|
||||||
|
|
||||||
|
(if (not value)
|
||||||
|
(corfu-hide-candidate-overlay) ;; when empty, we hide the overlay.
|
||||||
|
(pcase value ;; when not, we check the completion data.
|
||||||
|
(`(,fun ,beg ,end ,table . ,plist)
|
||||||
|
(let ((completion-in-region-mode-predicate
|
||||||
|
(lambda ()
|
||||||
|
(when-let (newbeg (car-safe (funcall fun)))
|
||||||
|
(= newbeg beg))))
|
||||||
|
(completion-extra-properties plist))
|
||||||
|
|
||||||
|
(setq completion-in-region--data
|
||||||
|
(list (if (markerp beg) beg (copy-marker beg))
|
||||||
|
(copy-marker end t)
|
||||||
|
table
|
||||||
|
(plist-get plist :predicate)))
|
||||||
|
|
||||||
|
(when (>= (- end beg) corfu-auto-prefix) ;; adhere to auto prefix length settings.
|
||||||
|
(corfu--update)
|
||||||
|
(let* ((candidate (car corfu--candidates))
|
||||||
|
(how-many-candidates (length corfu--candidates))
|
||||||
|
(len (- end beg))
|
||||||
|
(prefix (buffer-substring-no-properties beg end))
|
||||||
|
(suffix (substring candidate len)))
|
||||||
|
|
||||||
|
(if (and
|
||||||
|
;; need candidate
|
||||||
|
candidate
|
||||||
|
;; the prefix can't be empty (in case of corfu-auto-prefix equal 0)
|
||||||
|
(not (string-empty-p prefix))
|
||||||
|
;; prefix need to match the candidate as there are „fuzzy”
|
||||||
|
;; found candidates, esp. when using templates and the user
|
||||||
|
;; could see strage results at the first character.
|
||||||
|
(string-prefix-p prefix candidate))
|
||||||
|
|
||||||
|
(corfu--candidate-overlay-update
|
||||||
|
beg
|
||||||
|
end
|
||||||
|
prefix
|
||||||
|
suffix
|
||||||
|
how-many-candidates)
|
||||||
|
;; otherwise we hide the overlay.
|
||||||
|
(corfu-hide-candidate-overlay))))))))))
|
||||||
|
|
||||||
|
(defun corfu--candidate-overlay-post-command ()
|
||||||
|
"Post command hook updating the candidate overlay when user inserts character
|
||||||
|
and the cursor is at the end of word."
|
||||||
|
(let* ((is-insert-command
|
||||||
|
(corfu--match-symbol-p corfu-auto-commands this-command))
|
||||||
|
(is-delete-command
|
||||||
|
(corfu--match-symbol-p corfu-overlay-auto-commands this-command)))
|
||||||
|
(if (and
|
||||||
|
;; we are not in minibuffer, as it looks awkward.
|
||||||
|
(not (minibuffer-window-active-p (selected-window)))
|
||||||
|
(not (and ;; do not update it the point have not moved.
|
||||||
|
corfu--candidate-last-point
|
||||||
|
(= corfu--candidate-last-point (point))))
|
||||||
|
(or ;; do not update if it is not one of the insert or delete commands.
|
||||||
|
is-insert-command
|
||||||
|
is-delete-command))
|
||||||
|
(let ((next-char (char-after)))
|
||||||
|
(when (or ;; do not update if we are not at the end of the word.
|
||||||
|
(not next-char) ;; end of file
|
||||||
|
;; one of whitespace, quoting character, punctuation,
|
||||||
|
;; closing bracket, etc is next.
|
||||||
|
;; When those characters follow next completion won't trigger
|
||||||
|
;; eitherway: ' = * - + / ~ _ (have not investigated further)
|
||||||
|
(memq next-char '(?\s ?\t ?\r ?\n
|
||||||
|
?\" ?\` ?\) ?\] ?\>
|
||||||
|
?\. ?\, ?\: ?\;)))
|
||||||
|
;; When the completion backend is SLOW, i.e. like every LSP client,
|
||||||
|
;; then the overlay will not update and will interfere with the typing.
|
||||||
|
;; That's why we move preemptively when inserting and deleting the first
|
||||||
|
;; character (look awkward when typing a different word than the completion
|
||||||
|
;; but still looks better than flickering).
|
||||||
|
;; When deleting -- we just move the overlay so it will show
|
||||||
|
;; the „lagging” candidate.
|
||||||
|
(when (and is-insert-command corfu--candidate-overlay)
|
||||||
|
(let ((previous-text (overlay-get corfu--candidate-overlay 'after-string)))
|
||||||
|
(when (not (string-empty-p previous-text))
|
||||||
|
(overlay-put corfu--candidate-overlay 'after-string
|
||||||
|
(substring previous-text 1)))
|
||||||
|
(move-overlay corfu--candidate-overlay (point) (point))))
|
||||||
|
|
||||||
|
;; preserve the current position, show and update the overlay.
|
||||||
|
(setq corfu--candidate-last-point (point))
|
||||||
|
(corfu-show-candidate-overlay)))
|
||||||
|
;; or hide the overlay if the conditions where not met.
|
||||||
|
(corfu-hide-candidate-overlay))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(define-minor-mode corfu-candidate-overlay-mode
|
||||||
|
"Show first candidate in an overlay while typing."
|
||||||
|
:global t
|
||||||
|
:group 'corfu
|
||||||
|
(if corfu-candidate-overlay-mode
|
||||||
|
(progn
|
||||||
|
(add-hook 'post-command-hook #'corfu--candidate-overlay-post-command)
|
||||||
|
(message "Enabled `corfu-candidate-overlay-mode'."))
|
||||||
|
(progn
|
||||||
|
(remove-hook 'post-command-hook #'corfu--candidate-overlay-post-command)
|
||||||
|
(message "Disabled `corfu-candidate-overlay-mode'."))))
|
||||||
|
|
||||||
|
;;; corfu-candidate-overlay.el ends here
|
Loading…
Reference in a new issue