aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEshel Yaron2023-11-19 10:55:15 +0100
committerEli Zaretskii2023-11-25 12:06:03 +0200
commit3c3c46f4298fca9349fab080d974bdf7cdc7c25a (patch)
tree243a44c111754e7164930326f4c7fccdfe46e9ed
parentd5e6b3ff5ae0c1d52db848e56341b6299899fdd1 (diff)
downloademacs-3c3c46f4298fca9349fab080d974bdf7cdc7c25a.tar.gz
emacs-3c3c46f4298fca9349fab080d974bdf7cdc7c25a.zip
; Improve and add tests for Completion Preview mode
Fix handling of capfs that return a function or signal an error, respect the ':exclusive' completion property, fix lingering "exact" face after deletion that makes the matches non-exact, and add tests. * lisp/completion-preview.el (completion-preview--make-overlay): Only reuse the previous 'after-string' if it has the right face. (completion-preview--try-table) (completion-preview--capf-wrapper): New functions. (completion-preview--update): Use them. * test/lisp/completion-preview-tests.el: New file. (Bug#67275)
-rw-r--r--lisp/completion-preview.el107
-rw-r--r--test/lisp/completion-preview-tests.el184
2 files changed, 250 insertions, 41 deletions
diff --git a/lisp/completion-preview.el b/lisp/completion-preview.el
index 6048d5be272..95410e2e5cd 100644
--- a/lisp/completion-preview.el
+++ b/lisp/completion-preview.el
@@ -155,7 +155,9 @@ first candidate, and you can cycle between the candidates with
155 (setq completion-preview--overlay (make-overlay pos pos)) 155 (setq completion-preview--overlay (make-overlay pos pos))
156 (overlay-put completion-preview--overlay 'window (selected-window))) 156 (overlay-put completion-preview--overlay 'window (selected-window)))
157 (let ((previous (overlay-get completion-preview--overlay 'after-string))) 157 (let ((previous (overlay-get completion-preview--overlay 'after-string)))
158 (unless (and previous (string= previous string)) 158 (unless (and previous (string= previous string)
159 (eq (get-text-property 0 'face previous)
160 (get-text-property 0 'face string)))
159 (add-text-properties 0 1 '(cursor 1) string) 161 (add-text-properties 0 1 '(cursor 1) string)
160 (overlay-put completion-preview--overlay 'after-string string)) 162 (overlay-put completion-preview--overlay 'after-string string))
161 completion-preview--overlay)) 163 completion-preview--overlay))
@@ -178,48 +180,71 @@ first candidate, and you can cycle between the candidates with
178 (completion-preview-active-mode -1) 180 (completion-preview-active-mode -1)
179 (when (functionp func) (apply func args)))) 181 (when (functionp func) (apply func args))))
180 182
183(defun completion-preview--try-table (table beg end props)
184 "Check TABLE for a completion matching the text between BEG and END.
185
186PROPS is a property list with additional information about TABLE.
187See `completion-at-point-functions' for more details.
188
189If TABLE contains a matching completion, return a list
190\(PREVIEW BEG END ALL EXIT-FN) where PREVIEW is the text to show
191in the completion preview, ALL is the list of all matching
192completion candidates, and EXIT-FN is either a function to call
193after inserting PREVIEW or nil. If TABLE does not contain
194matching completions, or if there are multiple matching
195completions and `completion-preview-exact-match-only' is non-nil,
196return nil instead."
197 (let* ((pred (plist-get props :predicate))
198 (exit-fn (completion-preview--exit-function
199 (plist-get props :exit-function)))
200 (string (buffer-substring beg end))
201 (md (completion-metadata string table pred))
202 (sort-fn (or (completion-metadata-get md 'cycle-sort-function)
203 (completion-metadata-get md 'display-sort-function)
204 completion-preview-sort-function))
205 (all (let ((completion-lazy-hilit t))
206 (completion-all-completions string table pred
207 (- (point) beg) md)))
208 (last (last all))
209 (base (or (cdr last) 0))
210 (prefix (substring string base)))
211 (when last
212 (setcdr last nil)
213 (when-let ((sorted (funcall sort-fn
214 (delete prefix (all-completions prefix all)))))
215 (unless (and (cdr sorted) completion-preview-exact-match-only)
216 (list (propertize (substring (car sorted) (length prefix))
217 'face (if (cdr sorted)
218 'completion-preview
219 'completion-preview-exact))
220 (+ beg base) end sorted exit-fn))))))
221
222(defun completion-preview--capf-wrapper (capf)
223 "Translate return value of CAPF to properties for completion preview overlay."
224 (unless (eq capf #'completion-preview--insert)
225 (let ((res (ignore-errors (funcall capf))))
226 (and (consp res)
227 (not (functionp res))
228 (seq-let (beg end table &rest plist) res
229 (or (completion-preview--try-table table beg end plist)
230 (unless (eq 'no (plist-get plist :exclusive))
231 ;; Return non-nil to exclude other capfs.
232 '(nil))))))))
233
181(defun completion-preview--update () 234(defun completion-preview--update ()
182 "Update completion preview." 235 "Update completion preview."
183 (seq-let (beg end table &rest plist) 236 (seq-let (preview beg end all exit-fn)
184 (let ((completion-preview-insert-on-completion nil)) 237 (run-hook-wrapped
185 (run-hook-with-args-until-success 'completion-at-point-functions)) 238 'completion-at-point-functions
186 (when (and beg end table) 239 #'completion-preview--capf-wrapper)
187 (let* ((pred (plist-get plist :predicate)) 240 (when preview
188 (exit-fn (completion-preview--exit-function 241 (let ((ov (completion-preview--make-overlay end preview)))
189 (plist-get plist :exit-function))) 242 (overlay-put ov 'completion-preview-beg beg)
190 (string (buffer-substring beg end)) 243 (overlay-put ov 'completion-preview-end end)
191 (md (completion-metadata string table pred)) 244 (overlay-put ov 'completion-preview-index 0)
192 (sort-fn (or (completion-metadata-get md 'cycle-sort-function) 245 (overlay-put ov 'completion-preview-cands all)
193 (completion-metadata-get md 'display-sort-function) 246 (overlay-put ov 'completion-preview-exit-fn exit-fn)
194 completion-preview-sort-function)) 247 (completion-preview-active-mode)))))
195 (all (let ((completion-lazy-hilit t))
196 (completion-all-completions string table pred
197 (- (point) beg) md)))
198 (last (last all))
199 (base (or (cdr last) 0))
200 (bbeg (+ beg base))
201 (prefix (substring string base)))
202 (when last
203 (setcdr last nil)
204 (let* ((filtered (remove prefix (all-completions prefix all)))
205 (sorted (funcall sort-fn filtered))
206 (multi (cadr sorted)) ; multiple candidates
207 (cand (car sorted)))
208 (when (and cand
209 (not (and multi
210 completion-preview-exact-match-only)))
211 (let* ((face (if multi
212 'completion-preview
213 'completion-preview-exact))
214 (after (propertize (substring cand (length prefix))
215 'face face))
216 (ov (completion-preview--make-overlay end after)))
217 (overlay-put ov 'completion-preview-beg bbeg)
218 (overlay-put ov 'completion-preview-end end)
219 (overlay-put ov 'completion-preview-index 0)
220 (overlay-put ov 'completion-preview-cands sorted)
221 (overlay-put ov 'completion-preview-exit-fn exit-fn)
222 (completion-preview-active-mode)))))))))
223 248
224(defun completion-preview--show () 249(defun completion-preview--show ()
225 "Show a new completion preview. 250 "Show a new completion preview.
diff --git a/test/lisp/completion-preview-tests.el b/test/lisp/completion-preview-tests.el
new file mode 100644
index 00000000000..b5518e96254
--- /dev/null
+++ b/test/lisp/completion-preview-tests.el
@@ -0,0 +1,184 @@
1;;; completion-preview-tests.el --- tests for completion-preview.el -*- lexical-binding: t -*-
2
3;; Copyright (C) 2023 Free Software Foundation, Inc.
4
5;; This file is part of GNU Emacs.
6
7;; GNU Emacs is free software: you can redistribute it and/or modify
8;; it under the terms of the GNU General Public License as published by
9;; the Free Software Foundation, either version 3 of the License, or
10;; (at your option) any later version.
11
12;; GNU Emacs is distributed in the hope that it will be useful,
13;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;; GNU General Public License for more details.
16
17;; You should have received a copy of the GNU General Public License
18;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
19
20;;; Code:
21
22(require 'ert)
23(require 'completion-preview)
24
25(defun completion-preview-tests--capf (completions &rest props)
26 (lambda ()
27 (when-let ((bounds (bounds-of-thing-at-point 'symbol)))
28 (append (list (car bounds) (cdr bounds) completions) props))))
29
30(defun completion-preview-tests--check-preview (string &optional exact)
31 "Check that the completion preview is showing STRING.
32
33If EXACT is non-nil, check that STRING has the
34`completion-preview-exact' face. Otherwise check that STRING has
35the `completion-preview' face.
36
37If STRING is nil, check that there is no completion preview
38instead."
39 (if (not string)
40 (should (not completion-preview--overlay))
41 (should completion-preview--overlay)
42 (let ((after-string (completion-preview--get 'after-string)))
43 (should (string= after-string string))
44 (should (eq (get-text-property 0 'face after-string)
45 (if exact
46 'completion-preview-exact
47 'completion-preview))))))
48
49(ert-deftest completion-preview ()
50 "Test Completion Preview mode."
51 (with-temp-buffer
52 (setq-local completion-at-point-functions
53 (list (completion-preview-tests--capf '("foobarbaz"))))
54
55 (insert "foo")
56 (let ((this-command 'self-insert-command))
57 (completion-preview--post-command))
58
59 ;; Exact match
60 (completion-preview-tests--check-preview "barbaz" 'exact)
61
62 (insert "v")
63 (let ((this-command 'self-insert-command))
64 (completion-preview--post-command))
65
66 ;; No match, no preview
67 (completion-preview-tests--check-preview nil)
68
69 (delete-char -1)
70 (let ((this-command 'delete-backward-char))
71 (completion-preview--post-command))
72
73 ;; Exact match again
74 (completion-preview-tests--check-preview "barbaz" 'exact)))
75
76(ert-deftest completion-preview-multiple-matches ()
77 "Test Completion Preview mode with multiple matching candidates."
78 (with-temp-buffer
79 (setq-local completion-at-point-functions
80 (list (completion-preview-tests--capf
81 '("foobar" "foobaz"))))
82 (insert "foo")
83 (let ((this-command 'self-insert-command))
84 (completion-preview--post-command))
85
86 ;; Multiple matches, the preview shows the first one
87 (completion-preview-tests--check-preview "bar")
88
89 (completion-preview-next-candidate 1)
90
91 ;; Next match
92 (completion-preview-tests--check-preview "baz")))
93
94(ert-deftest completion-preview-exact-match-only ()
95 "Test `completion-preview-exact-match-only'."
96 (with-temp-buffer
97 (setq-local completion-at-point-functions
98 (list (completion-preview-tests--capf
99 '("spam" "foobar" "foobaz")))
100 completion-preview-exact-match-only t)
101 (insert "foo")
102 (let ((this-command 'self-insert-command))
103 (completion-preview--post-command))
104
105 ;; Multiple matches, so no preview
106 (completion-preview-tests--check-preview nil)
107
108 (delete-region (point-min) (point-max))
109 (insert "spa")
110 (let ((this-command 'self-insert-command))
111 (completion-preview--post-command))
112
113 ;; Exact match
114 (completion-preview-tests--check-preview "m" 'exact)))
115
116(ert-deftest completion-preview-function-capfs ()
117 "Test Completion Preview mode with capfs that return a function."
118 (with-temp-buffer
119 (setq-local completion-at-point-functions
120 (list
121 (lambda () #'ignore)
122 (completion-preview-tests--capf
123 '("foobar" "foobaz"))))
124 (insert "foo")
125 (let ((this-command 'self-insert-command))
126 (completion-preview--post-command))
127 (completion-preview-tests--check-preview "bar")))
128
129(ert-deftest completion-preview-non-exclusive-capfs ()
130 "Test Completion Preview mode with non-exclusive capfs."
131 (with-temp-buffer
132 (setq-local completion-at-point-functions
133 (list
134 (completion-preview-tests--capf
135 '("spam") :exclusive 'no)
136 (completion-preview-tests--capf
137 '("foobar" "foobaz") :exclusive 'no)
138 (completion-preview-tests--capf
139 '("foobarbaz"))))
140 (insert "foo")
141 (let ((this-command 'self-insert-command))
142 (completion-preview--post-command))
143 (completion-preview-tests--check-preview "bar")
144 (setq-local completion-preview-exact-match-only t)
145 (let ((this-command 'self-insert-command))
146 (completion-preview--post-command))
147 (completion-preview-tests--check-preview "barbaz" 'exact)))
148
149(ert-deftest completion-preview-face-updates ()
150 "Test updating the face in completion preview when match is no longer exact."
151 (with-temp-buffer
152 (setq-local completion-at-point-functions
153 (list
154 (completion-preview-tests--capf
155 '("foobarbaz" "food"))))
156 (insert "foo")
157 (let ((this-command 'self-insert-command))
158 (completion-preview--post-command))
159 (completion-preview-tests--check-preview "d")
160 (insert "b")
161 (let ((this-command 'self-insert-command))
162 (completion-preview--post-command))
163 (completion-preview-tests--check-preview "arbaz" 'exact)
164 (delete-char -1)
165 (let ((this-command 'delete-backward-char))
166 (completion-preview--post-command))
167 (completion-preview-tests--check-preview "d")))
168
169(ert-deftest completion-preview-capf-errors ()
170 "Test Completion Preview mode with capfs that signal errors.
171
172`dabbrev-capf' is one example of such a capf."
173 (with-temp-buffer
174 (setq-local completion-at-point-functions
175 (list
176 (lambda () (user-error "bad"))
177 (completion-preview-tests--capf
178 '("foobarbaz"))))
179 (insert "foo")
180 (let ((this-command 'self-insert-command))
181 (completion-preview--post-command))
182 (completion-preview-tests--check-preview "barbaz" 'exact)))
183
184;;; completion-preview-tests.el ends here