Added corfu-candidate-overlay-complete-at-point function

master
Adam Kruszewski 2023-07-04 10:54:43 +02:00
parent 56b5d12641
commit 6cdc33aa21
2 changed files with 111 additions and 87 deletions

View File

@ -5,7 +5,7 @@
;; Author: Adam Kruszewski <adam@kruszewski.name>
;; Maintainer: Adam Kruszewski <adam@kruszewski.name>
;; Created: 2023
;; Version: 1.3
;; Version: 1.4
;; Package-Requires: ((emacs "28.1") (corfu "0.36"))
;; Homepage: https://code.bsdgeek.org/adam/corfu-candidate-overlay/
@ -232,6 +232,20 @@ Otherwise the overlay can influence movement commands (i.e. the cursor is
is-delete-command))))
(corfu-candidate-overlay--hide)))))
(defun corfu-candidate-overlay-at-word-boundary-p ()
"Return non-nil when cursor is at the word boundary for completion purposes.
Or nil otherwise."
(let ((next-char (char-after)))
(or (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
;; either-way: ' = * - + / ~ _ (have not investigated further),
;; so they are not in the list.
(memq next-char '(?\s ?\t ?\r ?\n
?\" ?\` ?\) ?\] ?\>
?\. ?\, ?\: ?\;)))))
(defun corfu-candidate-overlay--post-command ()
"Post command hook to update candidate overlay.
Update happens when the user types character and the cursor is at
@ -242,90 +256,94 @@ the end of word."
;; dedicated to debugging those; but a timer corfu menu is using is much
;; more forgiving than how Emacs handle post and pre command hooks).
(ignore-errors
(let* ((is-insert-command
(corfu--match-symbol-p corfu-auto-commands this-command))
(is-delete-command
(corfu--match-symbol-p corfu-candidate-overlay-auto-commands this-command)))
;; short-circuit conditions -- the earlier we return if don't need to do
;; anything the better.
(if (and
;; we are not in minibuffer, as it looks awkward.
(not (minibuffer-window-active-p (selected-window)))
(not corfu-auto) ;; don't work with corfu-auto
;; corfu menu needs to be hidden
(not (and (frame-live-p corfu--frame) (frame-visible-p corfu--frame)))
(not (and ;; do not update if the point have not moved.
corfu-candidate-overlay--last-point
(= corfu-candidate-overlay--last-point (point))))
(or ;; do not update if it is not one of the insert or delete commands.
is-insert-command
is-delete-command))
;; now we check additional short-circuit conditions, but those operate on
;; next character.
(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
;; either-way: ' = * - + / ~ _ (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 often not update and will interfere with the typing.
;; 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--overlay ;; need overlay active
(let* ((candidate
(corfu-candidate-overlay--get-overlay-property 'corfu-candidate))
(prefix
(corfu-candidate-overlay--get-overlay-property 'corfu-prefix))
(count
(corfu-candidate-overlay--get-overlay-property 'corfu-count))
(previous-text
(corfu-candidate-overlay--get-overlay-property '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)
;; we still have some characters present in the prefix,
;; so we'll borrow one and move to the candidate.
(corfu-candidate-overlay--update
(point) ;; move to current cursor's position
(substring prefix 0 (- (length prefix) 1 ))
(concat (substring prefix (- (length prefix) 1)) candidate)
count)
;; if the length of prefix is zero then we can only hide
;; the overlay as we are removing past the current word
;; boundary.
(corfu-candidate-overlay--hide)))
;; 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
(if (and
(not (string= previous-text ""))
(length> candidate 1))
(corfu-candidate-overlay--update
(point) ;; move to current cursor's position
(concat prefix (substring candidate 0 1))
(substring candidate 1 (length candidate))
count)
;; no previous candidate or candidate is zero length,
;; probably we have reached the end of suggested word,
;; so let's hide the overlay.
(corfu-candidate-overlay--hide))))))
;; preserve the current position, show and update the overlay.
;; the corfu-candidate-overlay--show CAN be interrupted, that's why
;; we did the shuffling above.
(setq corfu-candidate-overlay--last-point (point))
(corfu-candidate-overlay--show)))
;; or hide the overlay if the conditions to show the overlay where not met.
(corfu-candidate-overlay--hide)))))
(let* ((is-insert-command
(corfu--match-symbol-p corfu-auto-commands this-command))
(is-delete-command
(corfu--match-symbol-p corfu-candidate-overlay-auto-commands this-command)))
;; short-circuit conditions -- the earlier we return, if don't need to do
;; anything, the better.
(if (and
;; we are not in minibuffer, as it looks awkward.
(not (minibuffer-window-active-p (selected-window)))
(not corfu-auto) ;; don't work with corfu-auto
;; corfu menu needs to be hidden
(not (and (frame-live-p corfu--frame) (frame-visible-p corfu--frame)))
(not (and ;; do not update if the point have not moved.
corfu-candidate-overlay--last-point
(= corfu-candidate-overlay--last-point (point))))
(or ;; do not update if it is not one of the insert or delete commands.
is-insert-command
is-delete-command)
;; check whether we are at word boundardy.
(corfu-candidate-overlay-at-word-boundary-p))
;; ...when main compound conditions is true clause...
(progn
;; When the completion backend is SLOW, i.e. like every LSP client,
;; then the overlay will often not update and will interfere with the typing.
;; 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--overlay
(let* ((candidate
(corfu-candidate-overlay--get-overlay-property 'corfu-candidate))
(prefix
(corfu-candidate-overlay--get-overlay-property 'corfu-prefix))
(count
(corfu-candidate-overlay--get-overlay-property 'corfu-count))
(previous-text
(corfu-candidate-overlay--get-overlay-property '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)
;; we still have some characters present in the prefix,
;; so we'll borrow one and move to the candidate.
(corfu-candidate-overlay--update
(point) ;; move to current cursor's position
(substring prefix 0 (- (length prefix) 1 ))
(concat (substring prefix (- (length prefix) 1)) candidate)
count)
;; if the length of prefix is zero then we can only hide
;; the overlay as we are removing past the current word
;; boundary.
(corfu-candidate-overlay--hide)))
;; 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
(if (and
(not (string= previous-text ""))
(length> candidate 1))
(corfu-candidate-overlay--update
(point) ;; move to current cursor's position
(concat prefix (substring candidate 0 1))
(substring candidate 1 (length candidate))
count)
;; no previous candidate or candidate is zero length,
;; probably we have reached the end of suggested word,
;; so let's hide the overlay.
(corfu-candidate-overlay--hide))))))
;; preserve the current position, show and update the overlay.
;; the corfu-candidate-overlay--show CAN be interrupted, that's why
;; we did the shuffling above.
(setq corfu-candidate-overlay--last-point (point))
(corfu-candidate-overlay--show))
;; ...when main compound conditions is false clause...
;; so hide the overlay if the conditions to show the overlay where not met.
(corfu-candidate-overlay--hide)))))
;;;###autoload
(defun corfu-candidate-overlay-complete-at-point ()
"Insert the first completion candidate shown in the overlay."
(interactive)
(when
(and corfu-candidate-overlay--overlay
(overlayp corfu-candidate-overlay--overlay)
(corfu-candidate-overlay-at-word-boundary-p))
(insert (corfu-candidate-overlay--get-overlay-property 'corfu-candidate))))
;;;###autoload
(define-minor-mode corfu-candidate-overlay-mode

View File

@ -12,6 +12,8 @@ Overlay showing auto-completion candidate when there is only one available (so i
[[./readme-images/corfu-candidate-overlay-one.png]]
It show the first auto-suggestion from [[https://github.com/minad/corfu][corfu]] and when there is only one it will show it underlined (face attributes can be configured). In case of many suggestions available, invoking =completion-at-point= will open the corfu popup menu to choose from, in case of single completion available (i.e. underlined) it will get auto-completed seeing the menu when invoked =completion-at-point=.
Package exposes also one interactive function =corfu-candidate-overlay-complete-at-point= which you can bind to a key combination. Invoking the function will complete the exact candidate overlay is currently showing[fn:4] (i.e. it will not show corfu's popup even if there are more candidates present, just complete what you see).
** Motivation
Similarly to Michell Hashimoto[fn:1] of HashiCorp fame, I like my editor to do less than more. I'm not as ascetic though ;-) From time to time I do use the auto-suggestion menu of [[https://github.com/minad/corfu][corfu]], especially when I forget the exact wording of a function. I do however trigger the auto-suggestion popup manually.
@ -40,8 +42,11 @@ When using slow or cpu-heavy corfu backends (e.g. [[https://github.com/minad/cap
;; enable corfu-candidate-overlay mode globally
;; this relies on having corfu-auto set to nil
(corfu-candidate-overlay-mode +1)
;; bind Ctr + TAB to trigger the completion popup of corfu
(global-set-key (kbd "C-<tab>") 'completion-at-point))
;; bind Ctrl + TAB to trigger the completion popup of corfu
(global-set-key (kbd "C-<tab>") 'completion-at-point)
;; bind Ctrl + Shift + Tab to trigger completion of the first candidate
;; (keybing <iso-lefttab> may not work for your keyboard model)
(global-set-key (kbd "C-<iso-lefttab>") 'corfu-candidate-overlay-complete-at-point))
#+end_src
** Customization
Faces available for customization:
@ -72,6 +77,7 @@ Issue reports, questions, comments and code patches are welcome -- you can send
If you haven't sent code patches via e-mail yet and would like to learn how to work with an e-mail based workflow, you can read more at [[https://git-scm.com/docs/git-format-patch][git format-patch]] man page or at [[https://git-send-email.io/][git-send-email.io]].
* Footnotes
[fn:4] Thanks to [[https://github.com/terlar][Terje Larsen]] for suggestion!
[fn:1] See video cast:
[[https://www.youtube.com/watch?v=rysgxl35EGc][Worst Practices in Software Development: Mitchell Hashimoto uses a simple code editor]].