Fixed error when no further candidate was found

When corfu--candidate was ended up as nil, the overlay still held
previous value but was not updated as the error was raised in
the update function.

Additionally when the backend is not keeping up we use
the previous candidate to update what the user see using
all of the information we had (i.e. prefix and the candidate),
so we borrow a character from one and append/prepend to the
other when necessary. This gives a little better experience
when using slow backend such as lsp.
This commit is contained in:
Adam Kruszewski 2023-05-07 17:06:36 +02:00
parent cfd74e2f6b
commit 446f88ff78

View file

@ -49,7 +49,7 @@
"Keymap to dismiss the Corfu candidate overlay.") "Keymap to dismiss the Corfu candidate overlay.")
(defcustom corfu-overlay-auto-commands (defcustom corfu-overlay-auto-commands
'("delete-backward-char\\'") '("delete-backward-char\\'" "backward-delete-char-untabify")
"Additional commands apart from corfu-auto-commands which initiate "Additional commands apart from corfu-auto-commands which initiate
completion candidate overlay." completion candidate overlay."
:type '(repeat (choice regexp symbol)) :type '(repeat (choice regexp symbol))
@ -91,6 +91,14 @@
(add-text-properties 0 1 '(cursor 1) candidate)) (add-text-properties 0 1 '(cursor 1) candidate))
(overlay-put corfu--candidate-overlay 'window (selected-window)) (overlay-put corfu--candidate-overlay 'window (selected-window))
;; we store the whole candidate and prefix as a property to use when
;; deleting characters in quick succession so the backend will not
;; keep-up. We will need to use those stored values then to still
;; show the overlay with a meaningful suggestion
;; (i.e. the last one found)
(overlay-put corfu--candidate-overlay 'corfu-candidate candidate)
(overlay-put corfu--candidate-overlay 'corfu-prefix prefix)
;; and here is the candidate string as it will be rendered by Emacs.
(overlay-put corfu--candidate-overlay 'after-string (overlay-put corfu--candidate-overlay 'after-string
(propertize (propertize
candidate candidate
@ -135,76 +143,145 @@
(when (>= (- end beg) corfu-auto-prefix) ;; adhere to auto prefix length settings. (when (>= (- end beg) corfu-auto-prefix) ;; adhere to auto prefix length settings.
(corfu--update) (corfu--update)
(let* ((candidate (car corfu--candidates)) ;; unfortunately corfu--candidates CAN and ARE nil sometimes,
(how-many-candidates (length corfu--candidates)) ;; if so we just short-circuit and hide the overlay.
(len (- end beg)) ;; TODO: Refactor the below to have just one ``if'' condition. Low priority.
(prefix (buffer-substring-no-properties beg end)) (if corfu--candidates
(suffix (substring candidate len))) (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, it should be present if we got here, but safety-first
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 so the user
;; could see strage results when typing the first character.
(string-prefix-p prefix candidate))
;; and finally we update the overlay.
(corfu--candidate-overlay-update
beg
end
prefix
suffix
how-many-candidates)
;; hide if the above ``if-condition'' is not met.
(corfu-hide-candidate-overlay)))
;; hide if there is no corfu--candidates at all (equals nil).
(corfu-hide-candidate-overlay)))))))))
(if (and (defun corfu--candidate-overlay-pre-command ()
;; need candidate "Pre command hook to hide the overlay if the command is not insert or delete.
candidate Otherwise the overlay can influence movement commands (i.e. the cursor is
;; the prefix can't be empty (in case of corfu-auto-prefix equal 0) considered to be located at the end of the overlay, so line movement will
(not (string-empty-p prefix)) jump to character far removed from the perceived cursor location)."
;; prefix need to match the candidate as there are „fuzzy” ;; We should not throw an error here, as Emacs will disable
;; found candidates, esp. when using templates and the user ;; the hook if it fails with an error.
;; could see strage results at the first character. (ignore-errors
(string-prefix-p prefix candidate)) (let* ((is-insert-command
(corfu--match-symbol-p corfu-auto-commands this-command))
(corfu--candidate-overlay-update (is-delete-command
beg (corfu--match-symbol-p corfu-overlay-auto-commands this-command)))
end (when (and
prefix ;; we are not in minibuffer.
suffix (not (minibuffer-window-active-p (selected-window)))
how-many-candidates) ;; and the command is not one of insert or delete.
;; otherwise we hide the overlay. (not (or
(corfu-hide-candidate-overlay)))))))))) is-insert-command
is-delete-command)))
(corfu-hide-candidate-overlay)))))
(defun corfu--candidate-overlay-post-command () (defun corfu--candidate-overlay-post-command ()
"Post command hook updating the candidate overlay when user inserts character "Post command hook updating the candidate overlay when user inserts character
and the cursor is at the end of word." and the cursor is at the end of word."
(let* ((is-insert-command ;; We should not throw an error here, as Emacs will disable
(corfu--match-symbol-p corfu-auto-commands this-command)) ;; the hook if it fails with an error (and auto suggestion backends
(is-delete-command ;; can and do throw errors sometimes, corfu even have a readme section
(corfu--match-symbol-p corfu-overlay-auto-commands this-command))) ;; dedicated to debugging those; but a timer corfu menu is using is much
(if (and ;; more forgiving than how Emacs handle post and pre command hooks).
;; we are not in minibuffer, as it looks awkward. (ignore-errors
(not (minibuffer-window-active-p (selected-window))) (let* ((is-insert-command
(not (and ;; do not update it the point have not moved. (corfu--match-symbol-p corfu-auto-commands this-command))
corfu--candidate-last-point (is-delete-command
(= corfu--candidate-last-point (point)))) (corfu--match-symbol-p corfu-overlay-auto-commands this-command)))
(or ;; do not update if it is not one of the insert or delete commands. ;; short-circuit conditions -- the earlier we return if don't need to do
is-insert-command ;; anything the better.
is-delete-command)) (if (and
(let ((next-char (char-after))) ;; we are not in minibuffer, as it looks awkward.
(when (or ;; do not update if we are not at the end of the word. (not (minibuffer-window-active-p (selected-window)))
(not next-char) ;; end of file (not (and ;; do not update if the point have not moved.
;; one of whitespace, quoting character, punctuation, corfu--candidate-last-point
;; closing bracket, etc is next. (= corfu--candidate-last-point (point))))
;; When those characters follow next completion won't trigger (or ;; do not update if it is not one of the insert or delete commands.
;; eitherway: ' = * - + / ~ _ (have not investigated further) is-insert-command
(memq next-char '(?\s ?\t ?\r ?\n is-delete-command))
?\" ?\` ?\) ?\] ?\> ;; now we check additional short-circuit conditions, but those operate on
?\. ?\, ?\: ?\;))) ;; next character.
;; When the completion backend is SLOW, i.e. like every LSP client, (let ((next-char (char-after)))
;; then the overlay will not update and will interfere with the typing. (when (or ;; do not update if we are not at the end of the word.
;; That's why we move preemptively when inserting and deleting the first (not next-char) ;; end of file
;; character (look awkward when typing a different word than the completion ;; one of whitespace, quoting character, punctuation,
;; but still looks better than flickering). ;; closing bracket, etc is next.
;; When deleting -- we just move the overlay so it will show ;; When those characters follow next completion won't trigger
;; the „lagging” candidate. ;; either-way: ' = * - + / ~ _ (have not investigated further)
(when (and is-insert-command corfu--candidate-overlay) (memq next-char '(?\s ?\t ?\r ?\n
(let ((previous-text (overlay-get corfu--candidate-overlay 'after-string))) ?\" ?\` ?\) ?\] ?\>
(when (not (string-empty-p previous-text)) ?\. ?\, ?\: ?\;)))
(overlay-put corfu--candidate-overlay 'after-string ;; When the completion backend is SLOW, i.e. like every LSP client,
(substring previous-text 1))) ;; then the overlay will often not update and will interfere with the typing.
(move-overlay corfu--candidate-overlay (point) (point)))) ;; That's why we operate on stored prefix and candidate giving an illusion
;; of updating the overlay -- but using the previous auto suggestion candidate.
(when corfu--candidate-overlay ;; need overlay active
(let* ((candidate
(overlay-get corfu--candidate-overlay 'corfu-candidate))
(prefix
(overlay-get corfu--candidate-overlay 'corfu-prefix))
(previous-text
(overlay-get corfu--candidate-overlay 'after-string)))
;; We need to deal with the overlay and stored candidate differently
;; when inserting and deleting (i.e. we need to shift one characte from or to
;; prefix to/from candidate)
(cond
;; TODO: Delete character case should probably be moved to pre-command hook.
(is-delete-command
(if (length> prefix 0)
(progn
;; we still have some characters present in the prefix,
;; so we'll borrow one and move to the candidate.
(overlay-put corfu--candidate-overlay 'corfu-candidate
(concat candidate (substring prefix (- (length prefix) 1))))
(overlay-put corfu--candidate-overlay 'corfu-prefix
(substring prefix 0 (- (length prefix) 1 )))
(overlay-put corfu--candidate-overlay 'after-string
(concat candidate (substring prefix (- (length prefix) 1))))
(move-overlay corfu--candidate-overlay (point) (point)))
;; if the length of prefix is zero then we can only hide
;; the overlay as we are removing past the current word
;; boundary.
(corfu-hide-candidate-overlay)))
;; Inserting character - we still update using historical data
;; in case the corfu backend would get interrupted;
;; Here we "borrow" a character from the candidate and append it to the prefix.
(is-insert-command
(when (not (string-empty-p previous-text))
(overlay-put corfu--candidate-overlay 'corfu-candidate
(and (length> candidate 1) (substring candidate 1 (- (length candidate) 1))))
(overlay-put corfu--candidate-overlay 'corfu-prefix
(concat prefix (and (length> candidate 1) (substring candidate 0 1))))
(overlay-put corfu--candidate-overlay 'after-string
(substring previous-text 1)))))
;; preserve the current position, show and update the overlay. (move-overlay corfu--candidate-overlay (point) (point))))
(setq corfu--candidate-last-point (point)) ;; preserve the current position, show and update the overlay.
(corfu-show-candidate-overlay))) ;; the corfu-show-candidate-overlay CAN be interrupted, that's why
;; or hide the overlay if the conditions where not met. ;; we did the shuffling above.
(corfu-hide-candidate-overlay)))) (setq corfu--candidate-last-point (point))
(corfu-show-candidate-overlay)))
;; or hide the overlay if the conditions to show the overlay where not met.
(corfu-hide-candidate-overlay)))))
;;;###autoload ;;;###autoload
(define-minor-mode corfu-candidate-overlay-mode (define-minor-mode corfu-candidate-overlay-mode
@ -214,9 +291,11 @@
(if corfu-candidate-overlay-mode (if corfu-candidate-overlay-mode
(progn (progn
(add-hook 'post-command-hook #'corfu--candidate-overlay-post-command) (add-hook 'post-command-hook #'corfu--candidate-overlay-post-command)
(add-hook 'pre-command-hook #'corfu--candidate-overlay-pre-command)
(message "Enabled `corfu-candidate-overlay-mode'.")) (message "Enabled `corfu-candidate-overlay-mode'."))
(progn (progn
(remove-hook 'post-command-hook #'corfu--candidate-overlay-post-command) (remove-hook 'post-command-hook #'corfu--candidate-overlay-post-command)
(remove-hook 'pre-command-hook #'corfu--candidate-overlay-pre-command)
(message "Disabled `corfu-candidate-overlay-mode'.")))) (message "Disabled `corfu-candidate-overlay-mode'."))))
;;; corfu-candidate-overlay.el ends here ;;; corfu-candidate-overlay.el ends here