after-change-functions -> buffer-modified-tick
[emacs-capf-autosuggest.git] / capf-autosuggest.el
blob70a0e277eb7b2b3eda2952a36929a9be40f32804
1 ;;; capf-autosuggest.el --- Show first completion-at-point as an overlay -*- lexical-binding: t; -*-
3 ;; Filename: capf-autosuggest.el
4 ;; Description: Show first completion-at-point as an overlay
5 ;; Author: jakanakaevangeli <jakanakaevangeli@chiru.no>
6 ;; Created: 2021-07-13
7 ;; Version: 1.0
8 ;; URL: https://github.com/jakanakaevangeli/emacs-capf-autosuggest
10 ;; This file is not part of GNU Emacs.
12 ;; This program is free software: you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ;;; Commentary:
27 ;; capf-autosuggest lets you preview the most recent matching history element,
28 ;; similar to zsh-autosuggestions or fish. It works in eshell and in modes
29 ;; derived from comint-mode, for example M-x shell or M-x run-python
31 ;; Installation:
33 ;; Add the following to your Emacs init file:
35 ;; (add-to-list 'load-path "/path/to/emacs-capf-autosuggest")
36 ;; (require 'capf-autosuggest)
37 ;; (add-hook 'comint-mode-hook #'capf-autosuggest-setup-comint)
38 ;; (add-hook 'eshell-mode-hook #'capf-autosuggest-setup-eshell)
40 ;; Configuration:
42 ;; Use `capf-autosuggest-define-partial-accept-cmd' to make a command that can
43 ;; move point into an auto-suggested layer.
45 ;; Example: to make C-M-f (forward-sexp) movable into suggested text, put the
46 ;; following into you init file:
48 ;; (with-eval-after-load 'capf-autosuggest
49 ;; (capf-autosuggest-define-partial-accept-cmd
50 ;; movable-forward-sexp forward-sexp)
51 ;; (define-key capf-autosuggest-active-mode-map [remap forward-sexp]
52 ;; #'movable-forward-sexp))
54 ;; Details:
56 ;; capf-autosuggest provides a minor mode, capf-autosuggest-mode, that lets you
57 ;; preview the first completion candidate for in-buffer completion as an
58 ;; overlay. Instead of using the default hook `completion-at-point-functions',
59 ;; it uses its own hook `capf-autosuggest-capf-functions'. However, by
60 ;; default, this hook contains a function that reads the default hook, but only
61 ;; if point is at end of line, because an auto-suggested overlay can be
62 ;; annoying in the middle of a line. If you want, you can try enabling this
63 ;; minor mode in an ordinary buffer for previewing tab completion candidates at
64 ;; end of line.
66 ;; completion-at-point functions for comint and eshell history are also
67 ;; provided. Because they are less useful for tab completion and more useful
68 ;; for auto-suggestion preview, they should be added to
69 ;; `capf-autosuggest-capf-functions', which doesn't interfere with tab
70 ;; completion. The setup functions mentioned in "Installation" paragraph add
71 ;; them to this hook locally. By default, if there are no matches for history
72 ;; completion and point is at end of line, we fall back to previewing the
73 ;; default tab completion candidates, as described in the previous paragraph.
75 ;; You can customize this behaviour by customizing
76 ;; `capf-autosuggest-capf-functions'. For example, you could add
77 ;; `capf-autosuggest-orig-capf' to enable auto-suggestions of tab completion
78 ;; candidates in the middle of a line.
80 ;; Alternatives:
82 ;; There is also esh-autosuggest[1] with similar functionality. Differences:
83 ;; it optionally allows having a delay, it depends on emacs package company, it
84 ;; is implemented only for eshell.
86 ;; [1]: http://github.com/dieggsy/esh-autosuggest
88 ;;; Code:
90 (require 'ring)
91 (eval-when-compile
92 (require 'subr-x)
93 (require 'cl-lib))
95 (defvar comint-input-ring)
96 (defvar comint-accum-marker)
97 (defvar comint-use-prompt-regexp)
98 (defvar eshell-history-ring)
99 (defvar eshell-last-output-end)
100 (declare-function eshell-bol "esh-mode")
101 (declare-function comint-previous-matching-input-from-input "comint")
102 (declare-function eshell-previous-matching-input-from-input "em-hist")
104 (defface capf-autosuggest-face '((t :inherit file-name-shadow))
105 "Face used for auto suggestions."
106 :group 'completion)
108 (defvar capf-autosuggest-capf-functions '(capf-autosuggest-orig-if-at-eol-capf)
109 "`completion-at-point-functions', used by capf-autossugest.
110 It is used instead of the standard
111 `completion-at-point-functions', but the default value contains
112 `capf-autosuggest-orig-if-at-eol-capf' which searches the
113 standard capf functions, if point is at the end of line.")
115 (defvar capf-autosuggest-all-completions-only-one nil
116 "Non-nil if only the first result of `all-completions' is of interest.
117 capf-autosuggest binds this to t around calls to
118 `all-completions'. A dynamic completion table can take this as a
119 hint to only return a list of one element for optimization.")
121 (defvar-local capf-autosuggest--overlay nil)
122 (defvar-local capf-autosuggest--str "")
123 (defvar-local capf-autosuggest--tick nil)
124 (defvar-local capf-autosuggest--region '(nil)
125 "Region of `completion-at-point'.")
127 (defun capf-autosuggest-orig-capf (&optional capf-functions)
128 "A capf that chooses from hook variable CAPF-FUNCTIONS.
129 CAPF-FUNCTIONS defaults to `completion-at-point-functions'.
130 Don't add this function to `completion-at-point-functions', as
131 this will result in an infinite loop. Useful for adding to
132 `capf-autosuggest-capf-functions', making it search the standard
133 capf functions."
134 (cdr (run-hook-wrapped (or capf-functions 'completion-at-point-functions)
135 #'completion--capf-wrapper 'all)))
137 (defun capf-autosuggest-orig-if-at-eol-capf ()
138 "`capf-autosuggest-orig-capf' if at the end of buffer.
139 Otherwise, return nil."
140 (when (eolp)
141 (capf-autosuggest-orig-capf)))
143 (defvar capf-autosuggest-active-mode)
145 (defun capf-autosuggest--post-h ()
146 "Create an auto-suggest overlay."
147 (if completion-in-region-mode
148 (capf-autosuggest-active-mode -1)
149 (when capf-autosuggest-active-mode
150 ;; `identity' is used to generate slightly faster byte-code
151 (pcase-let ((`(,beg . ,end) (identity capf-autosuggest--region)))
152 (unless (and (< beg (point) end)
153 (eq (buffer-modified-tick) capf-autosuggest--tick))
154 (capf-autosuggest-active-mode -1))))
156 (unless capf-autosuggest-active-mode
157 (pcase (capf-autosuggest-orig-capf 'capf-autosuggest-capf-functions)
158 (`(,beg ,end ,table . ,plist)
159 (let* ((pred (plist-get plist :predicate))
160 (string (buffer-substring-no-properties beg end))
161 ;; See `completion-emacs21-all-completions'
162 (base (car (completion-boundaries string table pred ""))))
163 (when-let*
164 ((completions
165 (let ((capf-autosuggest-all-completions-only-one t))
166 ;; Use `all-completions' rather than
167 ;; `completion-all-completions' to bypass completion styles
168 ;; and strictly match only on prefix. This makes sense here
169 ;; as we only use the string without the prefix for the
170 ;; overlay.
171 (all-completions string table pred)))
172 ;; `all-completions' may return strings that don't strictly
173 ;; match on our prefix. Ignore them.
174 ((string-prefix-p (substring string base) (car completions)))
175 (str (substring (car completions) (- end beg base)))
176 ((/= 0 (length str))))
177 (setq capf-autosuggest--region (cons beg end)
178 capf-autosuggest--str (copy-sequence str)
179 capf-autosuggest--tick (buffer-modified-tick))
180 (move-overlay capf-autosuggest--overlay end end)
181 (when (eq ?\n (aref str 0))
182 (setq str (concat " " str)))
183 (add-text-properties 0 1 (list 'cursor (length str)) str)
184 (add-face-text-property 0 (length str) 'capf-autosuggest-face t str)
185 (overlay-put capf-autosuggest--overlay 'after-string str)
186 (capf-autosuggest-active-mode))))))))
188 ;;;###autoload
189 (define-minor-mode capf-autosuggest-mode
190 "Auto-suggest first completion at point with an overlay."
191 :group 'completion
192 (if capf-autosuggest-mode
193 (progn
194 (setq capf-autosuggest--overlay (make-overlay (point) (point) nil t t))
195 (add-hook 'post-command-hook #'capf-autosuggest--post-h nil t)
196 (add-hook 'completion-in-region-mode-hook #'capf-autosuggest--post-h nil t))
197 (remove-hook 'post-command-hook #'capf-autosuggest--post-h t)
198 (remove-hook 'completion-in-region-mode-hook #'capf-autosuggest--post-h t)
199 (capf-autosuggest-active-mode -1)))
201 ;;;###autoload
202 (defmacro capf-autosuggest-define-partial-accept-cmd (name command)
203 "Define a command NAME.
204 It will call COMMAND interactively, allowing it to move point
205 into an auto-suggested overlay. COMMAND must not modify buffer.
206 NAME must not be called if variable
207 `capf-autosuggest-active-mode' is inactive. NAME is suitable for
208 binding in `capf-autosuggest-active-mode-map'."
209 `(defun ,name ()
210 ,(format "`%s', possibly moving point into an auto-suggested overlay."
211 command)
212 (interactive)
213 (capf-autosuggest-call-partial-accept-cmd #',command)))
215 (defun capf-autosuggest-call-partial-accept-cmd (command)
216 "Call COMMAND interactively, stepping into auto-suggested overlay.
217 Temporarily convert the overlay to buffer text and call COMMAND
218 interactively. Afterwards, the added text is deleted, but only
219 the portion after point. Additionally, if point is outside of
220 the added text, the whole text is deleted."
221 (let (beg end text)
222 (with-silent-modifications
223 (catch 'cancel-atomic-change
224 (atomic-change-group
225 (save-excursion
226 (goto-char (overlay-start capf-autosuggest--overlay))
227 (setq beg (point))
228 (insert-and-inherit capf-autosuggest--str)
229 (setq end (point)))
230 (call-interactively command)
231 (and (> (point) beg)
232 (<= (point) end)
233 (setq text (buffer-substring beg (point))))
234 (throw 'cancel-atomic-change nil))))
235 (when text
236 (if (= (point) beg)
237 (insert text)
238 (save-excursion
239 (goto-char beg)
240 (insert text))))))
242 (declare-function evil-forward-char "ext:evil-commands" nil t)
243 (declare-function evil-end-of-line "ext:evil-commands" nil t)
244 (declare-function evil-end-of-visual-line "ext:evil-commands" nil t)
245 (declare-function evil-end-of-line-or-visual-line "ext:evil-commands" nil t)
246 (declare-function evil-middle-of-visual-line "ext:evil-commands" nil t)
247 (declare-function evil-last-non-blank "ext:evil-commands" nil t)
248 (declare-function evil-forward-word-begin "ext:evil-commands" nil t)
249 (declare-function evil-forward-word-end "ext:evil-commands" nil t)
250 (declare-function evil-forward-WORD-begin "ext:evil-commands" nil t)
251 (declare-function evil-forward-WORD-end "ext:evil-commands" nil t)
253 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-forward-word forward-word)
254 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-forward-char forward-char)
255 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-forward-sexp forward-sexp)
256 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-end-of-line end-of-line)
257 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-move-end-of-line move-end-of-line)
258 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-end-of-visual-line end-of-visual-line)
259 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-forward-char evil-forward-char)
260 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-end-of-line evil-end-of-line)
261 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-end-of-visual-line evil-end-of-visual-line)
262 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-end-of-line-or-visual-line evil-end-of-line-or-visual-line)
263 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-middle-of-visual-line evil-middle-of-visual-line)
264 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-last-non-blank evil-last-non-blank)
265 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-forward-word-begin evil-forward-word-begin)
266 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-forward-word-end evil-forward-word-end)
267 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-forward-WORD-begin evil-forward-WORD-begin)
268 (capf-autosuggest-define-partial-accept-cmd capf-autosuggest-evil-forward-WORD-end evil-forward-WORD-end)
270 (defun capf-autosuggest-accept ()
271 "Accept current auto-suggestion.
272 Do not call this command if variable `capf-autosuggest-active-mode' is
273 inactive."
274 (interactive)
275 (capf-autosuggest-call-partial-accept-cmd
276 (lambda ()
277 (interactive)
278 (goto-char (overlay-start capf-autosuggest--overlay)))))
280 (defvar capf-autosuggest-active-mode-map
281 (let ((map (make-sparse-keymap)))
282 (define-key map [remap forward-word] #'capf-autosuggest-forward-word)
283 (define-key map [remap forward-char] #'capf-autosuggest-forward-char)
284 (define-key map [remap forward-sexp] #'capf-autosuggest-forward-sexp)
285 (define-key map [remap end-of-line] #'capf-autosuggest-end-of-line)
286 (define-key map [remap move-end-of-line] #'capf-autosuggest-move-end-of-line)
287 (define-key map [remap end-of-visual-line] #'capf-autosuggest-end-of-visual-line)
289 (define-key map [remap evil-forward-char] #'capf-autosuggest-evil-forward-char)
290 (define-key map [remap evil-end-of-line] #'capf-autosuggest-evil-end-of-line)
291 (define-key map [remap evil-end-of-visual-line] #'capf-autosuggest-evil-end-of-visual-line)
292 (define-key map [remap evil-end-of-line-or-visual-line] #'capf-autosuggest-evil-end-of-line-or-visual-line)
293 (define-key map [remap evil-middle-of-visual-line] #'capf-autosuggest-evil-middle-of-visual-line)
294 (define-key map [remap evil-last-non-blank] #'capf-autosuggest-evil-last-non-blank)
295 (define-key map [remap evil-forward-word-begin] #'capf-autosuggest-evil-forward-word-begin)
296 (define-key map [remap evil-forward-word-end] #'capf-autosuggest-evil-forward-word-end)
297 (define-key map [remap evil-forward-WORD-begin] #'capf-autosuggest-evil-forward-WORD-begin)
298 (define-key map [remap evil-forward-WORD-end] #'capf-autosuggest-evil-forward-WORD-end)
300 (define-key map [remap eshell-previous-matching-input-from-input]
301 #'capf-autosuggest-eshell-previous-matching-input-from-input)
302 (define-key map [remap comint-previous-matching-input-from-input]
303 #'capf-autosuggest-comint-previous-matching-input-from-input)
304 map)
305 "Keymap active when an auto-suggestion is shown.")
307 (define-minor-mode capf-autosuggest-active-mode
308 "Active when auto-suggested overlay is shown."
309 :group 'completion
310 (unless capf-autosuggest-active-mode
311 (delete-overlay capf-autosuggest--overlay)))
313 ;;;###autoload
314 (defun capf-autosuggest-comint-capf ()
315 "Completion-at-point function for comint input history.
316 Is only applicable if point is after the last prompt."
317 (let ((ring comint-input-ring)
318 (beg nil))
319 (and ring (ring-p ring) (not (ring-empty-p ring))
320 (or (and (setq beg comint-accum-marker)
321 (setq beg (marker-position beg)))
322 (and (setq beg (get-buffer-process (current-buffer)))
323 (setq beg (marker-position (process-mark beg)))))
324 (>= (point) beg)
325 (list beg (if comint-use-prompt-regexp
326 (line-end-position)
327 (field-end))
328 (capf-autosuggest--completion-table ring)
329 :exclusive 'no))))
331 ;;;###autoload
332 (defun capf-autosuggest-eshell-capf ()
333 "Completion-at-point function for eshell input history.
334 Is only applicable if point is after the last prompt."
335 (let ((ring eshell-history-ring)
336 (beg nil))
337 (and ring (ring-p ring) (not (ring-empty-p ring))
338 (setq beg eshell-last-output-end)
339 (setq beg (marker-position beg))
340 (>= (point) beg)
341 (list (save-excursion (eshell-bol) (point)) (point-max)
342 (capf-autosuggest--completion-table ring)
343 :exclusive 'no))))
345 (defun capf-autosuggest--completion-table (ring)
346 "Return a completion table to complete on RING."
347 (let (self)
348 (setq
349 self
350 (lambda (input predicate action)
351 (cond
352 ((eq action t)
353 (cl-loop
354 with only-one = capf-autosuggest-all-completions-only-one
355 with regexps = completion-regexp-list
356 for i below (ring-size ring)
357 for elem = (ring-ref ring i)
358 if (string-prefix-p input elem)
359 if (cl-loop for regex in regexps
360 always (string-match-p regex elem))
361 if (or (null predicate)
362 (funcall predicate elem))
363 if only-one
364 return (list elem)
365 else collect elem))
366 ((eq action nil)
367 (complete-with-action
368 nil (let ((capf-autosuggest-all-completions-only-one nil))
369 (funcall self input predicate t))
370 input predicate))
371 ((eq action 'lambda)
372 (and (ring-member ring input)
373 (or (null predicate)
374 (funcall predicate input))
375 (cl-loop for regex in completion-regexp-list
376 always (string-match-p regex input))))
377 (t (complete-with-action
378 action (ring-elements ring) input predicate)))))))
380 ;;;###autoload
381 (defun capf-autosuggest-setup-comint ()
382 "Setup capf-autosuggest for history suggestion in comint."
383 (capf-autosuggest-mode)
384 (add-hook 'capf-autosuggest-capf-functions #'capf-autosuggest-comint-capf nil t))
386 ;;;###autoload
387 (defun capf-autosuggest-setup-eshell ()
388 "Setup capf-autosuggest for history suggestion in eshell."
389 (capf-autosuggest-mode)
390 (add-hook 'capf-autosuggest-capf-functions #'capf-autosuggest-eshell-capf nil t))
392 (defun capf-autosuggest-comint-previous-matching-input-from-input (n)
393 "Like `comint-previous-matching-input-from-input'.
394 But increase arument N by 1, if positive, but not on command
395 repetition."
396 (interactive "p")
397 (and (not (memq last-command '(comint-previous-matching-input-from-input
398 comint-next-matching-input-from-input)))
399 (> n 0)
400 (setq n (1+ n)))
401 (comint-previous-matching-input-from-input n)
402 (setq this-command #'comint-previous-matching-input-from-input))
404 (defun capf-autosuggest-eshell-previous-matching-input-from-input (n)
405 "Like `eshell-previous-matching-input-from-input'.
406 But increase arument N by 1, if positive, but not on command
407 repetition."
408 (interactive "p")
409 (and (not (memq last-command '(eshell-previous-matching-input-from-input
410 eshell-next-matching-input-from-input)))
411 (> n 0)
412 (setq n (1+ n)))
413 (eshell-previous-matching-input-from-input n)
414 (setq this-command #'eshell-previous-matching-input-from-input))
416 (provide 'capf-autosuggest)
417 ;;; capf-autosuggest.el ends here