/
slynk-completion.lisp
428 lines (392 loc) · 18.9 KB
/
slynk-completion.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
;;; slynk-flex-completion.lisp --- Common Lisp symbol completion routines
;;
;; Authors: João Távora, some parts derivative works of SLIME, by its
;; authors.
;;
(defpackage :slynk-completion
(:use #:cl #:slynk-api)
(:export
#:flex-completions
#:simple-completions
#:flex-matches))
;; for testing package-local nicknames
#+sbcl
(defpackage :slynk-completion-local-nicknames-test
(:use #:cl)
(:local-nicknames (#:api #:slynk-api)))
(in-package :slynk-completion)
;;; Simple completion
;;;
(defslyfun simple-completions (prefix package)
"Return a list of completions for the string PREFIX."
(let ((strings (all-simple-completions prefix package)))
(list strings (longest-common-prefix strings))))
(eval-when (:compile-toplevel :load-toplevel :execute)
(import 'simple-completions :slynk)
(export 'simple-completions :slynk))
(defun all-simple-completions (prefix package)
(multiple-value-bind (name pname intern) (tokenize-symbol prefix)
(let* ((extern (and pname (not intern)))
(pkg (cond ((equal pname "") +keyword-package+)
((not pname) (guess-buffer-package package))
(t (guess-package pname))))
(test (lambda (sym) (prefix-match-p name (symbol-name sym))))
(syms (and pkg (matching-symbols pkg extern test)))
(strings (loop for sym in syms
for str = (unparse-symbol sym)
when (prefix-match-p name str) ; remove |Foo|
collect str)))
(format-completion-set strings intern pname))))
(defun matching-symbols (package external test)
(let ((test (if external
(lambda (s)
(and (symbol-external-p s package)
(funcall test s)))
test))
(result '()))
(do-symbols (s package)
(when (funcall test s)
(push s result)))
(remove-duplicates result)))
(defun unparse-symbol (symbol)
(let ((*print-case* (case (readtable-case *readtable*)
(:downcase :upcase)
(t :downcase))))
(unparse-name (symbol-name symbol))))
(defun prefix-match-p (prefix string)
"Return true if PREFIX is a prefix of STRING."
(not (mismatch prefix string :end2 (min (length string) (length prefix))
:test #'char-equal)))
(defun longest-common-prefix (strings)
"Return the longest string that is a common prefix of STRINGS."
(if (null strings)
""
(flet ((common-prefix (s1 s2)
(let ((diff-pos (mismatch s1 s2)))
(if diff-pos (subseq s1 0 diff-pos) s1))))
(reduce #'common-prefix strings))))
(defun format-completion-set (strings internal-p package-name)
"Format a set of completion strings.
Returns a list of completions with package qualifiers if needed."
(mapcar (lambda (string) (untokenize-symbol package-name internal-p string))
(sort strings #'string<)))
;;; Fancy "flex" completion
;;;
(defmacro collecting ((&rest collectors) &body body) ; lifted from uiop
"COLLECTORS should be a list of names for collections. A collector
defines a function that, when applied to an argument inside BODY, will
add its argument to the corresponding collection. Returns multiple values,
a list for each collection, in order.
E.g.,
\(collecting \(foo bar\)
\(dolist \(x '\(\(a 1\) \(b 2\) \(c 3\)\)\)
\(foo \(first x\)\)
\(bar \(second x\)\)\)\)
Returns two values: \(A B C\) and \(1 2 3\)."
(let ((vars (mapcar #'(lambda (x) (gensym (symbol-name x))) collectors))
(initial-values (mapcar (constantly nil) collectors)))
`(let ,(mapcar #'list vars initial-values)
(flet ,(mapcar #'(lambda (c v) `(,c (x) (push x ,v) (values))) collectors vars)
,@body
(values ,@(mapcar #'(lambda (v) `(reverse ,v)) vars))))))
(defun to-chunks (string indexes)
"Return chunks of STRING in as specified by INDEXES."
;; (to-chunks "farfalhini" '(1 2 3 4)) => ((1 "arfa"))
;; (to-chunks "farfalhini" '(1 3 4)) => ((1 "a") (3 "fa"))
;; (to-chunks "farfalhini" '(1 2 3 4 5 7 8 9)) => ((1 "arfal") (7 "ini"))
;; (to-chunks "farfalhini" '(1 2 3 4 5 6 7 8 9)) => ((1 "arfalhini"))
(reverse (reduce (lambda (chunk-list number)
(let ((latest-chunk (car chunk-list)))
(if (and latest-chunk
(= (+
(length (second latest-chunk))
(first latest-chunk))
number))
(progn (setf (second latest-chunk)
(format nil "~a~c" (second latest-chunk)
(aref string number)))
chunk-list)
(cons (list number (format nil "~c" (aref string number)))
chunk-list))))
indexes
:initial-value nil)))
(defun readably-classify (sym)
(let* ((translations '((:fboundp . "fn")
(:class . "cla")
(:typespec . "type")
(:generic-function . "generic-fn")
(:macro . "macro")
(:special-operator . "special-op")
(:package . "pak")
(:boundp . "var")
(:constant . "constant")))
(classes (slynk::classify-symbol sym))
(classes (if (some (lambda (m) (member m classes)) '(:generic-function :macro))
(delete :fboundp classes)
classes))
(translated (mapcar (lambda (cla) (cdr (assoc cla translations)))
classes)))
(format nil "~{~a~^,~}" translated)))
(defparameter *flex-score-falloff* 1.5
"The larger the value, the more big index distances are penalized.")
(defparameter *more-qualified-matches* t
"If non-nil, \"foo\" more likely completes to \"bar:foo\".
Specifically this assigns a \"foo\" on \"bar:foo\" a
higher-than-usual score, as if the package qualifier \"bar\" was
shorter.")
(defun flex-score (string indexes pattern)
"Score the match of STRING as given by INDEXES.
INDEXES as calculated by FLEX-MATCHES."
(let* ((first-pattern-colon (and pattern
(position #\: pattern)))
(index-of-first-pattern-colon (and first-pattern-colon
(elt indexes first-pattern-colon)))
(first-string-colon)
(string-length (length string)))
(cond ((and first-pattern-colon
(plusp first-pattern-colon))
;; If the user included a colon (":") in the pattern, score
;; the pre-colon and post-colon parts separately and add
;; the resulting halves together. This tends to fare
;; slightly better when matching qualified symbols.
(let ((package-designator-score
(flex-score-1 index-of-first-pattern-colon
(subseq indexes 0 first-pattern-colon)))
(symbol-name-score
(flex-score-1 (- string-length
index-of-first-pattern-colon)
(mapcar (lambda (index)
(- index index-of-first-pattern-colon))
(subseq indexes (1+ first-pattern-colon))))))
(+ (/ package-designator-score 2)
(/ symbol-name-score 2))))
((and
*more-qualified-matches*
(setf first-string-colon (position #\: string))
(< first-string-colon
(car indexes)))
;; If the user did not include a colon, but the string
;; we're matching again does have that colon (we're
;; matching a qualified name), and the position of that
;; colon happens to be less than the first index, then act
;; as if the pre-colon part were actually half the size of
;; what it is. This also tends to promote qualified matches
;; meant on the symbol-name.
(let ((adjust (truncate (/ first-string-colon 2))))
(flex-score-1 (- string-length
adjust)
(mapcar (lambda (idx)
(- idx adjust))
indexes))))
(t
;; the default: score the whole pattern on the whole
;; string.
(flex-score-1 string-length indexes)))))
(defun flex-score-1 (string-length indexes)
"Does the real work of FLEX-SCORE.
Given that INDEXES is a list of integer position of characters in a
string of length STRING-LENGTH, say how well these characters
represent that STRING. There is a non-linear falloff with the
distances between the indexes, according to *FLEX-SCORE-FALLOFF*. If
that value is 2, for example, the indices '(0 1 2) on a 3-long
string of is a perfect (100% match,) while '(0 2) on that same
string is a 33% match and just '(1) is a 11% match."
(float
(/ (length indexes)
(* string-length
(+ 1 (reduce #'+
(loop for i from 0
for (a b) on `(,-1
,@indexes
,string-length)
while b
collect (expt (- b a 1) *flex-score-falloff*))))))))
(defun flex-matches (pattern string char-test)
"Return non-NIL if PATTERN flex-matches STRING.
In case of a match, return two values:
A list of non-negative integers which are the indexes of the
characters in PATTERN as found consecutively in STRING. This list
measures in length the number of characters in PATTERN.
A floating-point score. Higher scores for better matches."
(declare (optimize (speed 3) (safety 0))
(type simple-string string)
(type simple-string pattern)
(type function char-test))
(let* ((strlen (length string))
(indexes (loop for char across pattern
for from = 0 then (1+ pos)
for pos = (loop for i from from below strlen
when (funcall char-test
(aref string i) char)
return i)
unless pos
return nil
collect pos)))
(values indexes
(and indexes
(flex-score string indexes pattern)))))
(defun collect-if-matches (collector pattern string symbol)
"Make and collect a match with COLLECTOR if PATTERN matches STRING.
A match is a list (STRING SYMBOL INDEXES SCORE).
Return non-nil if match was collected, nil otherwise."
(multiple-value-bind (indexes score)
(flex-matches pattern string #'char=)
(when indexes
(funcall collector
(list string
symbol
indexes
score)))))
(defun sort-by-score (matches)
"Sort MATCHES by SCORE, highest score first.
Matches are produced by COLLECT-IF-MATCHES (which see)."
(sort matches #'> :key #'fourth))
(defun keywords-matching (pattern)
"Find keyword symbols flex-matching PATTERN.
Return an unsorted list of matches.
Matches are produced by COLLECT-IF-MATCHES (which see)."
(collecting (collect)
(and (char= (aref pattern 0) #\:)
(do-symbols (s +keyword-package+)
(collect-if-matches #'collect pattern (concatenate 'simple-string ":"
(symbol-name s))
s)))))
(defun accessible-matching (pattern package)
"Find symbols flex-matching PATTERN accessible without package-qualification.
Return an unsorted list of matches.
Matches are produced by COLLECT-IF-MATCHES (which see)."
(and (not (find #\: pattern))
(collecting (collect)
(let ((collected (make-hash-table)))
(do-symbols (s package)
;; XXX: since DO-SYMBOLS may visit a symbol more than
;; once. Read similar note apropos DO-ALL-SYMBOLS in
;; QUALIFIED-MATCHING for how we do it.
(collect-if-matches
(lambda (thing)
(unless (gethash s collected)
(setf (gethash s collected) t)
(funcall #'collect thing)))
pattern (symbol-name s) s))))))
(defun qualified-matching (pattern home-package)
"Find package-qualified symbols flex-matching PATTERN.
Return, as two values, a set of matches for external symbols,
package-qualified using one colon, and another one for internal
symbols, package-qualified using two colons.
The matches in the two sets are not guaranteed to be in their final
order, i.e. they are not sorted (except for the fact that
qualifications with shorter package nicknames are tried first).
Matches are produced by COLLECT-IF-MATCHES (which see)."
(let* ((first-colon (position #\: pattern))
(starts-with-colon (and first-colon (zerop first-colon)))
(two-colons (and first-colon (< (1+ first-colon) (length pattern))
(eq #\: (aref pattern (1+ first-colon))))))
(if (and starts-with-colon
(not two-colons))
(values nil nil)
(let* ((package-local-nicknames
(slynk-backend:package-local-nicknames home-package))
(package-local-nicknames-by-package
(let ((ret (make-hash-table)))
(loop for (short . full) in
package-local-nicknames
do (push short (gethash (find-package full)
ret)))
ret))
(nicknames-by-package (make-hash-table)))
(flet ((sorted-nicknames (package)
(or (gethash package nicknames-by-package)
(setf (gethash package nicknames-by-package)
(sort (append
(gethash package package-local-nicknames-by-package)
(package-nicknames package)
(list (package-name package)))
#'<
:key #'length)))))
(collecting (collect-external collect-internal)
(cond
(two-colons
(let ((collected (make-hash-table)))
(do-all-symbols (s)
(loop
with package = (symbol-package s)
for nickname in (and package ; gh#226
(sorted-nicknames package))
do (collect-if-matches
(lambda (thing)
;; XXX: since DO-ALL-SYMBOLS may visit
;; a symbol more than once, we want to
;; avoid double collections. But
;; instead of marking every traversed
;; symbol in a hash table, we mark just
;; those collected. We do pay an added
;; price of checking matching duplicate
;; symbols, but the much smaller hash
;; table pays off when benchmarked,
;; because the number of collections is
;; generally much smaller than the
;; total number of symbols.
(unless (gethash s collected)
(setf (gethash s collected) t)
(funcall #'collect-internal thing)))
pattern
(concatenate 'simple-string
nickname
"::"
(symbol-name s))
s)))))
(t
(loop
with use-list = (package-use-list home-package)
for package in (remove +keyword-package+ (list-all-packages))
for sorted-nicknames
= (and (not (eq package home-package))
(sorted-nicknames package))
do (when sorted-nicknames
(do-external-symbols (s package)
;;; XXX: This condition is slightly
;;; opinionated. It says, for example, that
;;; you never want to complete "c:del" to
;;; "cl:delete" or "common-lisp:delete" in
;;; packages that use :CL (a very common
;;; case).
(when (or first-colon
(not (member (symbol-package s) use-list)))
(loop for nickname in sorted-nicknames
do (collect-if-matches #'collect-external
pattern
(concatenate 'simple-string
nickname
":"
(symbol-name s))
s))))))))))))))
(defslyfun flex-completions (pattern package-name &key (limit 300))
"Compute \"flex\" completions for PATTERN given current PACKAGE-NAME.
Returns a list of (COMPLETIONS NIL). COMPLETIONS is a list of
\(STRING SCORE CHUNKS CLASSIFICATION-STRING)."
(when (plusp (length pattern))
(list (loop
with package = (guess-buffer-package package-name)
with upcasepat = (string-upcase pattern)
for (string symbol indexes score)
in
(loop with (external internal)
= (multiple-value-list (qualified-matching upcasepat package))
for e in (append (sort-by-score
(keywords-matching upcasepat))
(sort-by-score
(append (accessible-matching upcasepat package)
external))
(sort-by-score
internal))
for i upto limit
collect e)
collect
(list (if (every #'common-lisp:upper-case-p pattern)
(string-upcase string)
(string-downcase string))
score
(to-chunks string indexes)
(readably-classify symbol)))
nil)))
(provide :slynk/completion)