Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
marvel
GitHub Repository: marvel/qnf
Path: blob/master/elisp/slime/contrib/swank-fuzzy.lisp
990 views
1
;;; swank-fuzzy.lisp --- fuzzy symbol completion
2
;;
3
;; Authors: Brian Downing <[email protected]>
4
;; Tobias C. Rittweiler <[email protected]>
5
;; and others
6
;;
7
;; License: Public Domain
8
;;
9
10
11
(in-package :swank)
12
13
(eval-when (:compile-toplevel :load-toplevel :execute)
14
(swank-require :swank-c-p-c))
15
16
;;; For nomenclature of the fuzzy completion section, please read
17
;;; through the following docstring.
18
19
(defslimefun fuzzy-completions (string default-package-name &key limit time-limit-in-msec)
20
"Returns a list of two values:
21
22
An (optionally limited to LIMIT best results) list of fuzzy
23
completions for a symbol designator STRING. The list will be
24
sorted by score, most likely match first.
25
26
A flag that indicates whether or not TIME-LIMIT-IN-MSEC has
27
been exhausted during computation. If that parameter's value is
28
NIL or 0, no time limit is assumed.
29
30
The main result is a list of completion objects, where a completion
31
object is:
32
33
(COMPLETED-STRING SCORE (&rest CHUNKS) CLASSIFICATION-STRING)
34
35
where a CHUNK is a description of a matched substring:
36
37
(OFFSET SUBSTRING)
38
39
and FLAGS is short string describing properties of the symbol (see
40
SYMBOL-CLASSIFICATION-STRING).
41
42
E.g., completing \"mvb\" in a package that uses COMMON-LISP would
43
return something like:
44
45
((\"multiple-value-bind\" 26.588236 ((0 \"m\") (9 \"v\") (15 \"b\"))
46
(:FBOUNDP :MACRO))
47
...)
48
49
If STRING is package qualified the result list will also be
50
qualified. If string is non-qualified the result strings are
51
also not qualified and are considered relative to
52
DEFAULT-PACKAGE-NAME.
53
54
Which symbols are candidates for matching depends on the symbol
55
designator's format. The cases are as follows:
56
FOO - Symbols accessible in the buffer package.
57
PKG:FOO - Symbols external in package PKG.
58
PKG::FOO - Symbols accessible in package PKG."
59
;; For Emacs we allow both NIL and 0 as value of TIME-LIMIT-IN-MSEC
60
;; to denote an infinite time limit. Internally, we only use NIL for
61
;; that purpose, to be able to distinguish between "no time limit
62
;; alltogether" and "current time limit already exhausted." So we've
63
;; got to canonicalize its value at first:
64
(let* ((no-time-limit-p (or (not time-limit-in-msec) (zerop time-limit-in-msec)))
65
(time-limit (if no-time-limit-p nil time-limit-in-msec)))
66
(multiple-value-bind (completion-set interrupted-p)
67
(fuzzy-completion-set string default-package-name :limit limit
68
:time-limit-in-msec time-limit)
69
;; We may send this as elisp [] arrays to spare a coerce here,
70
;; but then the network serialization were slower by handling arrays.
71
;; Instead we limit the number of completions that is transferred
72
;; (the limit is set from Emacs.)
73
(list (coerce completion-set 'list) interrupted-p))))
74
75
76
;;; A Fuzzy Matching -- Not to be confused with a fuzzy completion
77
;;; object that will be sent back to Emacs, as described above.
78
79
(defstruct (fuzzy-matching (:conc-name fuzzy-matching.)
80
(:predicate fuzzy-matching-p)
81
(:constructor %make-fuzzy-matching))
82
symbol ; The symbol that has been found to match.
83
package-name ; The name of the package where SYMBOL was found in.
84
; (This is not necessarily the same as the home-package
85
; of SYMBOL, because the SYMBOL can be internal to
86
; lots of packages; also think of package nicknames.)
87
score ; The higher the better SYMBOL is a match.
88
package-chunks ; Chunks pertaining to the package identifier of SYMBOL.
89
symbol-chunks) ; Chunks pertaining to SYMBOL's name.
90
91
(defun make-fuzzy-matching (symbol package-name score package-chunks symbol-chunks)
92
(declare (inline %make-fuzzy-matching))
93
(%make-fuzzy-matching :symbol symbol :package-name package-name :score score
94
:package-chunks package-chunks
95
:symbol-chunks symbol-chunks))
96
97
(defun %fuzzy-extract-matching-info (fuzzy-matching user-input-string)
98
(multiple-value-bind (_ user-package-name __ input-internal-p)
99
(parse-completion-arguments user-input-string nil)
100
(declare (ignore _ __))
101
(with-struct (fuzzy-matching. score symbol package-name package-chunks symbol-chunks)
102
fuzzy-matching
103
(let (symbol-name real-package-name internal-p)
104
(cond (symbol ; symbol fuzzy matching?
105
(setf symbol-name (symbol-name symbol))
106
(setf internal-p input-internal-p)
107
(setf real-package-name (cond ((keywordp symbol) "")
108
((not user-package-name) nil)
109
(t package-name))))
110
(t ; package fuzzy matching?
111
(setf symbol-name "")
112
(setf real-package-name package-name)
113
;; If no explicit package name was given by the user
114
;; (e.g. input was "asdf"), we want to append only
115
;; one colon ":" to the package names.
116
(setf internal-p (if user-package-name input-internal-p nil))))
117
(values symbol-name
118
real-package-name
119
(if user-package-name internal-p nil)
120
(completion-output-symbol-converter user-input-string)
121
(completion-output-package-converter user-input-string))))))
122
123
(defun fuzzy-format-matching (fuzzy-matching user-input-string)
124
"Returns the completion (\"foo:bar\") that's represented by FUZZY-MATCHING."
125
(multiple-value-bind (symbol-name package-name internal-p symbol-converter package-converter)
126
(%fuzzy-extract-matching-info fuzzy-matching user-input-string)
127
(setq symbol-name (and symbol-name (funcall symbol-converter symbol-name)))
128
(setq package-name (and package-name (funcall package-converter package-name)))
129
(let ((result (untokenize-symbol package-name internal-p symbol-name)))
130
;; We return the length of the possibly added prefix as second value.
131
(values result (search symbol-name result)))))
132
133
(defun fuzzy-convert-matching-for-emacs (fuzzy-matching user-input-string)
134
"Converts a result from the fuzzy completion core into something
135
that emacs is expecting. Converts symbols to strings, fixes case
136
issues, and adds information (as a string) describing if the symbol is
137
bound, fbound, a class, a macro, a generic-function, a
138
special-operator, or a package."
139
(with-struct (fuzzy-matching. symbol score package-chunks symbol-chunks) fuzzy-matching
140
(multiple-value-bind (name added-length)
141
(fuzzy-format-matching fuzzy-matching user-input-string)
142
(list name
143
(format nil "~,2f" score)
144
(append package-chunks
145
(mapcar #'(lambda (chunk)
146
;; Fix up chunk positions to account for possible
147
;; added package identifier.
148
(let ((offset (first chunk)) (string (second chunk)))
149
(list (+ added-length offset) string)))
150
symbol-chunks))
151
(symbol-classification-string symbol)))))
152
153
(defun fuzzy-completion-set (string default-package-name &key limit time-limit-in-msec)
154
"Returns two values: an array of completion objects, sorted by
155
their score, that is how well they are a match for STRING
156
according to the fuzzy completion algorithm. If LIMIT is set,
157
only the top LIMIT results will be returned. Additionally, a flag
158
is returned that indicates whether or not TIME-LIMIT-IN-MSEC was
159
exhausted."
160
(check-type limit (or null (integer 0 #.(1- most-positive-fixnum))))
161
(check-type time-limit-in-msec (or null (integer 0 #.(1- most-positive-fixnum))))
162
(multiple-value-bind (matchings interrupted-p)
163
(fuzzy-generate-matchings string default-package-name time-limit-in-msec)
164
(when (and limit
165
(> limit 0)
166
(< limit (length matchings)))
167
(if (array-has-fill-pointer-p matchings)
168
(setf (fill-pointer matchings) limit)
169
(setf matchings (make-array limit :displaced-to matchings))))
170
(map-into matchings #'(lambda (m)
171
(fuzzy-convert-matching-for-emacs m string))
172
matchings)
173
(values matchings interrupted-p)))
174
175
176
(defun fuzzy-generate-matchings (string default-package-name time-limit-in-msec)
177
"Does all the hard work for FUZZY-COMPLETION-SET. If
178
TIME-LIMIT-IN-MSEC is NIL, an infinite time limit is assumed."
179
(multiple-value-bind (parsed-symbol-name parsed-package-name package internal-p)
180
(parse-completion-arguments string default-package-name)
181
(flet ((fix-up (matchings parent-package-matching)
182
;; The components of each matching in MATCHINGS have been computed
183
;; relatively to PARENT-PACKAGE-MATCHING. Make them absolute.
184
(let* ((p parent-package-matching)
185
(p.name (fuzzy-matching.package-name p))
186
(p.score (fuzzy-matching.score p))
187
(p.chunks (fuzzy-matching.package-chunks p)))
188
(map-into matchings
189
#'(lambda (m)
190
(let ((m.score (fuzzy-matching.score m)))
191
(setf (fuzzy-matching.package-name m) p.name)
192
(setf (fuzzy-matching.package-chunks m) p.chunks)
193
(setf (fuzzy-matching.score m)
194
(if (equal parsed-symbol-name "")
195
;; (Make package matchings be sorted before all the
196
;; relative symbol matchings while preserving over
197
;; all orderness.)
198
(/ p.score 100)
199
(+ p.score m.score)))
200
m))
201
matchings)))
202
(find-symbols (designator package time-limit &optional filter)
203
(fuzzy-find-matching-symbols designator package
204
:time-limit-in-msec time-limit
205
:external-only (not internal-p)
206
:filter (or filter #'identity)))
207
(find-packages (designator time-limit)
208
(fuzzy-find-matching-packages designator :time-limit-in-msec time-limit)))
209
(let ((time-limit time-limit-in-msec) (symbols) (packages) (results))
210
(cond ((not parsed-package-name) ; E.g. STRING = "asd"
211
;; We don't know if user is searching for a package or a symbol
212
;; within his current package. So we try to find either.
213
(setf (values packages time-limit) (find-packages parsed-symbol-name time-limit))
214
(setf (values symbols time-limit) (find-symbols parsed-symbol-name package time-limit)))
215
((string= parsed-package-name "") ; E.g. STRING = ":" or ":foo"
216
(setf (values symbols time-limit) (find-symbols parsed-symbol-name package time-limit)))
217
(t ; E.g. STRING = "asd:" or "asd:foo"
218
;; Find fuzzy matchings of the denoted package identifier part.
219
;; After that, find matchings for the denoted symbol identifier
220
;; relative to all the packages found.
221
(multiple-value-bind (found-packages rest-time-limit)
222
(find-packages parsed-package-name time-limit-in-msec)
223
;; We want to traverse the found packages in the order of their score,
224
;; since those with higher score presumably represent better choices.
225
;; (This is important because some packages may never be looked at if
226
;; time limit exhausts during traversal.)
227
(setf found-packages (sort found-packages #'fuzzy-matching-greaterp))
228
(loop
229
for package-matching across found-packages
230
for package = (find-package (fuzzy-matching.package-name package-matching))
231
while (or (not time-limit) (> rest-time-limit 0)) do
232
(multiple-value-bind (matchings remaining-time)
233
;; The duplication filter removes all those symbols which are
234
;; present in more than one package match. Specifically if such a
235
;; package match represents the home package of the symbol, it's
236
;; the one kept because this one is deemed to be the best match.
237
(find-symbols parsed-symbol-name package rest-time-limit
238
(%make-duplicate-symbols-filter
239
(remove package-matching found-packages)))
240
(setf matchings (fix-up matchings package-matching))
241
(setf symbols (concatenate 'vector symbols matchings))
242
(setf rest-time-limit remaining-time)
243
(let ((guessed-sort-duration (%guess-sort-duration (length symbols))))
244
(when (<= rest-time-limit guessed-sort-duration)
245
(decf rest-time-limit guessed-sort-duration)
246
(loop-finish))))
247
finally
248
(setf time-limit rest-time-limit)
249
(when (equal parsed-symbol-name "") ; E.g. STRING = "asd:"
250
(setf packages found-packages))))))
251
;; Sort by score; thing with equal score, sort alphabetically.
252
;; (Especially useful when PARSED-SYMBOL-NAME is empty, and all possible
253
;; completions are to be returned.)
254
(setf results (concatenate 'vector symbols packages))
255
(setf results (sort results #'fuzzy-matching-greaterp))
256
(values results (and time-limit (<= time-limit 0)))))))
257
258
(defun %guess-sort-duration (length)
259
;; These numbers are pretty much arbitrary, except that they're
260
;; vaguely correct on my machine with SBCL. Yes, this is an ugly
261
;; kludge, but it's better than before (where this didn't exist at
262
;; all, which essentially meant, that this was taken to be 0.)
263
(if (zerop length)
264
0
265
(let ((comparasions (* 3.8 (* length (log length 2)))))
266
(* 1000 (* comparasions (expt 10 -7)))))) ; msecs
267
268
(defun %make-duplicate-symbols-filter (fuzzy-package-matchings)
269
;; Returns a filter function that takes a symbol, and which returns T
270
;; if and only if /no/ matching in FUZZY-PACKAGE-MATCHINGS represents
271
;; the home-package of the symbol passed.
272
(let ((packages (mapcar #'(lambda (m)
273
(find-package (fuzzy-matching.package-name m)))
274
(coerce fuzzy-package-matchings 'list))))
275
#'(lambda (symbol)
276
(not (member (symbol-package symbol) packages)))))
277
278
(defun fuzzy-matching-greaterp (m1 m2)
279
"Returns T if fuzzy-matching M1 should be sorted before M2.
280
Basically just the scores of the two matchings are compared, and
281
the match with higher score wins. For the case that the score is
282
equal, the one which comes alphabetically first wins."
283
(declare (type fuzzy-matching m1 m2))
284
(let ((score1 (fuzzy-matching.score m1))
285
(score2 (fuzzy-matching.score m2)))
286
(cond ((> score1 score2) t)
287
((< score1 score2) nil) ; total order
288
(t
289
(let ((name1 (symbol-name (fuzzy-matching.symbol m1)))
290
(name2 (symbol-name (fuzzy-matching.symbol m2))))
291
(string< name1 name2))))))
292
293
(declaim (ftype (function () (integer 0)) get-real-time-msecs))
294
(defun get-real-time-in-msecs ()
295
(let ((units-per-msec (max 1 (floor internal-time-units-per-second 1000))))
296
(values (floor (get-internal-real-time) units-per-msec)))) ; return just one value!
297
298
(defun fuzzy-find-matching-symbols
299
(string package &key (filter #'identity) external-only time-limit-in-msec)
300
"Returns two values: a vector of fuzzy matchings for matching
301
symbols in PACKAGE, using the fuzzy completion algorithm, and the
302
remaining time limit.
303
304
Only those symbols are considered of which FILTER does return T.
305
306
If EXTERNAL-ONLY is true, only external symbols are considered. A
307
TIME-LIMIT-IN-MSEC of NIL is considered no limit; if it's zero or
308
negative, perform a NOP."
309
(let ((time-limit-p (and time-limit-in-msec t))
310
(time-limit (or time-limit-in-msec 0))
311
(rtime-at-start (get-real-time-in-msecs))
312
(package-name (package-name package))
313
(count 0))
314
(declare (type boolean time-limit-p))
315
(declare (type integer time-limit rtime-at-start))
316
(declare (type (integer 0 #.(1- most-positive-fixnum)) count))
317
318
(flet ((recompute-remaining-time (old-remaining-time)
319
(cond ((not time-limit-p)
320
(values nil nil)) ; propagate NIL back as infinite time limit.
321
((> count 0) ; ease up on getting internal time like crazy.
322
(setf count (mod (1+ count) 128))
323
(values nil old-remaining-time))
324
(t (let* ((elapsed-time (- (get-real-time-in-msecs) rtime-at-start))
325
(remaining (- time-limit elapsed-time)))
326
(values (<= remaining 0) remaining)))))
327
(perform-fuzzy-match (string symbol-name)
328
(let* ((converter (completion-output-symbol-converter string))
329
(converted-symbol-name (funcall converter symbol-name)))
330
(compute-highest-scoring-completion string converted-symbol-name))))
331
(let ((completions (make-array 256 :adjustable t :fill-pointer 0))
332
(rest-time-limit time-limit))
333
(block loop
334
(do-symbols* (symbol package)
335
(multiple-value-bind (exhausted? remaining-time)
336
(recompute-remaining-time rest-time-limit)
337
(setf rest-time-limit remaining-time)
338
(cond (exhausted? (return-from loop))
339
((or (not external-only) (symbol-external-p symbol package))
340
(when (funcall filter symbol)
341
(if (string= "" string) ; "" matches always
342
(vector-push-extend (make-fuzzy-matching symbol package-name
343
0.0 '() '())
344
completions)
345
(multiple-value-bind (match-result score)
346
(perform-fuzzy-match string (symbol-name symbol))
347
(when match-result
348
(vector-push-extend
349
(make-fuzzy-matching symbol package-name score
350
'() match-result)
351
completions))))))))))
352
(values completions rest-time-limit)))))
353
354
355
(defun fuzzy-find-matching-packages (name &key time-limit-in-msec)
356
"Returns a vector of fuzzy matchings for each package that is
357
similiar to NAME, and the remaining time limit.
358
Cf. FUZZY-FIND-MATCHING-SYMBOLS."
359
(let ((time-limit-p (and time-limit-in-msec t))
360
(time-limit (or time-limit-in-msec 0))
361
(rtime-at-start (get-real-time-in-msecs))
362
(converter (completion-output-package-converter name))
363
(completions (make-array 32 :adjustable t :fill-pointer 0)))
364
(declare (type boolean time-limit-p))
365
(declare (type integer time-limit rtime-at-start))
366
(declare (type function converter))
367
(if (and time-limit-p (<= time-limit 0))
368
(values #() time-limit)
369
(loop for package in (list-all-packages) do
370
;; Find best-matching package-nickname:
371
(loop with max-pkg-name = ""
372
with max-result = nil
373
with max-score = 0
374
for package-name in (package-names package)
375
for converted-name = (funcall converter package-name)
376
do
377
(multiple-value-bind (result score)
378
(compute-highest-scoring-completion name converted-name)
379
(when (and result (> score max-score))
380
(setf max-pkg-name package-name)
381
(setf max-result result)
382
(setf max-score score)))
383
finally
384
(when max-result
385
(vector-push-extend (make-fuzzy-matching nil max-pkg-name
386
max-score max-result '())
387
completions)))
388
finally
389
(return
390
(values completions
391
(and time-limit-p
392
(let ((elapsed-time (- (get-real-time-in-msecs) rtime-at-start)))
393
(- time-limit elapsed-time)))))))))
394
395
396
(defslimefun fuzzy-completion-selected (original-string completion)
397
"This function is called by Slime when a fuzzy completion is
398
selected by the user. It is for future expansion to make
399
testing, say, a machine learning algorithm for completion scoring
400
easier.
401
402
ORIGINAL-STRING is the string the user completed from, and
403
COMPLETION is the completion object (see docstring for
404
SWANK:FUZZY-COMPLETIONS) corresponding to the completion that the
405
user selected."
406
(declare (ignore original-string completion))
407
nil)
408
409
410
;;;;; Fuzzy completion core
411
412
(defparameter *fuzzy-recursion-soft-limit* 30
413
"This is a soft limit for recursion in
414
RECURSIVELY-COMPUTE-MOST-COMPLETIONS. Without this limit,
415
completing a string such as \"ZZZZZZ\" with a symbol named
416
\"ZZZZZZZZZZZZZZZZZZZZZZZ\" will result in explosive recursion to
417
find all the ways it can match.
418
419
Most natural language searches and symbols do not have this
420
problem -- this is only here as a safeguard.")
421
(declaim (fixnum *fuzzy-recursion-soft-limit*))
422
423
(defun compute-highest-scoring-completion (short full)
424
"Finds the highest scoring way to complete the abbreviation
425
SHORT onto the string FULL, using CHAR= as a equality function for
426
letters. Returns two values: The first being the completion
427
chunks of the highest scorer, and the second being the score."
428
(let* ((scored-results
429
(mapcar #'(lambda (result)
430
(cons (score-completion result short full) result))
431
(compute-most-completions short full)))
432
(winner (first (sort scored-results #'> :key #'first))))
433
(values (rest winner) (first winner))))
434
435
(defun compute-most-completions (short full)
436
"Finds most possible ways to complete FULL with the letters in SHORT.
437
Calls RECURSIVELY-COMPUTE-MOST-COMPLETIONS recursively. Returns
438
a list of (&rest CHUNKS), where each CHUNKS is a description of
439
how a completion matches."
440
(let ((*all-chunks* nil))
441
(declare (special *all-chunks*))
442
(recursively-compute-most-completions short full 0 0 nil nil nil t)
443
*all-chunks*))
444
445
(defun recursively-compute-most-completions
446
(short full
447
short-index initial-full-index
448
chunks current-chunk current-chunk-pos
449
recurse-p)
450
"Recursively (if RECURSE-P is true) find /most/ possible ways
451
to fuzzily map the letters in SHORT onto FULL, using CHAR= to
452
determine if two letters match.
453
454
A chunk is a list of elements that have matched consecutively.
455
When consecutive matches stop, it is coerced into a string,
456
paired with the starting position of the chunk, and pushed onto
457
CHUNKS.
458
459
Whenever a letter matches, if RECURSE-P is true,
460
RECURSIVELY-COMPUTE-MOST-COMPLETIONS calls itself with a position
461
one index ahead, to find other possibly higher scoring
462
possibilities. If there are less than
463
*FUZZY-RECURSION-SOFT-LIMIT* results in *ALL-CHUNKS* currently,
464
this call will also recurse.
465
466
Once a word has been completely matched, the chunks are pushed
467
onto the special variable *ALL-CHUNKS* and the function returns."
468
(declare ;(optimize speed)
469
(fixnum short-index initial-full-index)
470
(simple-string short full)
471
(special *all-chunks*))
472
(flet ((short-cur ()
473
"Returns the next letter from the abbreviation, or NIL
474
if all have been used."
475
(if (= short-index (length short))
476
nil
477
(aref short short-index)))
478
(add-to-chunk (char pos)
479
"Adds the CHAR at POS in FULL to the current chunk,
480
marking the start position if it is empty."
481
(unless current-chunk
482
(setf current-chunk-pos pos))
483
(push char current-chunk))
484
(collect-chunk ()
485
"Collects the current chunk to CHUNKS and prepares for
486
a new chunk."
487
(when current-chunk
488
(push (list current-chunk-pos
489
(coerce (reverse current-chunk) 'string)) chunks)
490
(setf current-chunk nil
491
current-chunk-pos nil))))
492
;; If there's an outstanding chunk coming in collect it. Since
493
;; we're recursively called on skipping an input character, the
494
;; chunk can't possibly continue on.
495
(when current-chunk (collect-chunk))
496
(do ((pos initial-full-index (1+ pos)))
497
((= pos (length full)))
498
(let ((cur-char (aref full pos)))
499
(if (and (short-cur)
500
(char= cur-char (short-cur)))
501
(progn
502
(when recurse-p
503
;; Try other possibilities, limiting insanely deep
504
;; recursion somewhat.
505
(recursively-compute-most-completions
506
short full short-index (1+ pos)
507
chunks current-chunk current-chunk-pos
508
(not (> (length *all-chunks*)
509
*fuzzy-recursion-soft-limit*))))
510
(incf short-index)
511
(add-to-chunk cur-char pos))
512
(collect-chunk))))
513
(collect-chunk)
514
;; If we've exhausted the short characters we have a match.
515
(if (short-cur)
516
nil
517
(let ((rev-chunks (reverse chunks)))
518
(push rev-chunks *all-chunks*)
519
rev-chunks))))
520
521
522
;;;;; Fuzzy completion scoring
523
524
(defparameter *fuzzy-completion-symbol-prefixes* "*+-%&?<"
525
"Letters that are likely to be at the beginning of a symbol.
526
Letters found after one of these prefixes will be scored as if
527
they were at the beginning of ths symbol.")
528
(defparameter *fuzzy-completion-symbol-suffixes* "*+->"
529
"Letters that are likely to be at the end of a symbol.
530
Letters found before one of these suffixes will be scored as if
531
they were at the end of the symbol.")
532
(defparameter *fuzzy-completion-word-separators* "-/."
533
"Letters that separate different words in symbols. Letters
534
after one of these symbols will be scores more highly than other
535
letters.")
536
537
(defun score-completion (completion short full)
538
"Scores the completion chunks COMPLETION as a completion from
539
the abbreviation SHORT to the full string FULL. COMPLETION is a
540
list like:
541
((0 \"mul\") (9 \"v\") (15 \"b\"))
542
Which, if SHORT were \"mulvb\" and full were \"multiple-value-bind\",
543
would indicate that it completed as such (completed letters
544
capitalized):
545
MULtiple-Value-Bind
546
547
Letters are given scores based on their position in the string.
548
Letters at the beginning of a string or after a prefix letter at
549
the beginning of a string are scored highest. Letters after a
550
word separator such as #\- are scored next highest. Letters at
551
the end of a string or before a suffix letter at the end of a
552
string are scored medium, and letters anywhere else are scored
553
low.
554
555
If a letter is directly after another matched letter, and its
556
intrinsic value in that position is less than a percentage of the
557
previous letter's value, it will use that percentage instead.
558
559
Finally, a small scaling factor is applied to favor shorter
560
matches, all other things being equal."
561
(labels ((at-beginning-p (pos)
562
(= pos 0))
563
(after-prefix-p (pos)
564
(and (= pos 1)
565
(find (aref full 0) *fuzzy-completion-symbol-prefixes*)))
566
(word-separator-p (pos)
567
(find (aref full pos) *fuzzy-completion-word-separators*))
568
(after-word-separator-p (pos)
569
(find (aref full (1- pos)) *fuzzy-completion-word-separators*))
570
(at-end-p (pos)
571
(= pos (1- (length full))))
572
(before-suffix-p (pos)
573
(and (= pos (- (length full) 2))
574
(find (aref full (1- (length full)))
575
*fuzzy-completion-symbol-suffixes*)))
576
(score-or-percentage-of-previous (base-score pos chunk-pos)
577
(if (zerop chunk-pos)
578
base-score
579
(max base-score
580
(+ (* (score-char (1- pos) (1- chunk-pos)) 0.85)
581
(expt 1.2 chunk-pos)))))
582
(score-char (pos chunk-pos)
583
(score-or-percentage-of-previous
584
(cond ((at-beginning-p pos) 10)
585
((after-prefix-p pos) 10)
586
((word-separator-p pos) 1)
587
((after-word-separator-p pos) 8)
588
((at-end-p pos) 6)
589
((before-suffix-p pos) 6)
590
(t 1))
591
pos chunk-pos))
592
(score-chunk (chunk)
593
(loop for chunk-pos below (length (second chunk))
594
for pos from (first chunk)
595
summing (score-char pos chunk-pos))))
596
(let* ((chunk-scores (mapcar #'score-chunk completion))
597
(length-score (/ 10.0 (1+ (- (length full) (length short))))))
598
(values
599
(+ (reduce #'+ chunk-scores) length-score)
600
(list (mapcar #'list chunk-scores completion) length-score)))))
601
602
(defun highlight-completion (completion full)
603
"Given a chunk definition COMPLETION and the string FULL,
604
HIGHLIGHT-COMPLETION will create a string that demonstrates where
605
the completion matched in the string. Matches will be
606
capitalized, while the rest of the string will be lower-case."
607
(let ((highlit (nstring-downcase (copy-seq full))))
608
(dolist (chunk completion)
609
(setf highlit (nstring-upcase highlit
610
:start (first chunk)
611
:end (+ (first chunk)
612
(length (second chunk))))))
613
highlit))
614
615
(defun format-fuzzy-completion-set (winners)
616
"Given a list of completion objects such as on returned by
617
FUZZY-COMPLETION-SET, format the list into user-readable output
618
for interactive debugging purpose."
619
(let ((max-len
620
(loop for winner in winners maximizing (length (first winner)))))
621
(loop for (sym score result) in winners do
622
(format t "~&~VA score ~8,2F ~A"
623
max-len (highlight-completion result sym) score result))))
624
625
(provide :swank-fuzzy)
626