The attached patch makes Slime's fuzzy completion semantically complete. For example on SBCL, "sb:with C-c M-i" will now display all with-style macros of all sb-* packages!
Here is an extract of the resulting *Fuzzy Completions* buffer (the output in the real buffer is even cooler, as the relevant matchings of substrings of "sb:with" are highlighted):
Completion: Flags: Score: --------------------------------------------- ------- -------- sb-c:with-source-location -f--m-- 62.97 sb-ext:with-timeout -f--m-- 62.16 sb-sys:without-gcing -f--m-- 62.05 sb-alien:with-alien -f--m-- 61.90 sb-sys:with-fd-handler -f--m-- 61.88 sb-sys:with-interrupts -f--m-- 61.88 sb-int:with-unique-names -f--m-- 61.76 sb-thread:with-mutex -f--m-- 61.72 sb-sys:without-interrupts -f--m-- 61.71 sb-int:with-sane-io-syntax -f--m-- 61.67 sb-sys:with-pinned-objects -f--m-- 61.67 sb-ext:without-package-locks -f--m-- 61.60 sb-ext:with-unlocked-packages -f--m-- 61.57 sb-int:with-rebound-io-syntax -f--m-- 61.57 sb-int:with-float-traps-masked -f--m-- 61.55 sb-sys:with-enabled-interrupts ------- 61.55 sb-unix:with-restarted-syscall -f--m-- 61.24 sb-assem:without-scheduling -f--m-- 61.14 sb-kernel:with-array-data -f--m-- 61.13 sb-kernel:%with-array-data -f----- 61.06 sb-thread:with-new-session -f--m-- 61.06 sb-thread:with-recursive-lock -f--m-- 60.92 sb-kernel:%with-array-data-macro -f--m-- 60.82 sb-kernel:with-circularity-detection -f--m-- 60.73 sb-kernel:with-single-package-locked-error -f--m-- 60.64 swank-backend:with-struct -f--m-- 59.43 sb-c:lambda-with-lexenv ------- 56.67 ...
The patch depends on my two previous patches. Appliable via -p0. Tested on SBCL and CLISP.
-T.
--- ../slime-hacked-1/ChangeLog 2007-03-03 15:26:51.000000000 +0100 +++ ChangeLog 2007-03-03 16:18:18.000000000 +0100 @@ -1,3 +1,48 @@ +2007-03-03 Tobias C. Rittweiler tcr@freebits.de + + * swank.lisp: Making fuzzy completion semantically right from a + user perspective. As an example on SBCL, "sb:with- C-c M-i" will + display all exported "with"-style macros in all sb-* packages from + now on. :) + + (parse-completion-arguments): Replacing with a semantically-sound + implementation Previous implementation was a bit, because the + previous one was a bit confused. Clarifying docstring. Adding + commentary table of various constellations of returned values + for thorough explanation. + + (carefully-find-package): Removed. Obsolete by above change. + + (defstruct fuzzy-matching): Introduced to make internally-used + datastructure explicit. Distinguishing ``completion chunks'' + between those pertaining to the symbol itself and those to the + package identifier. + + (convert-fuzzy-completion-result): Renamed to + FUZZY-CONVERT-MATCHING-FOR-EMACS. + + (fuzzy-convert-matching-for-emacs): Accomodating for the new + datastructure. Only the chunks pertaining to the symbol itself are + fixed up positionally, the package-chunks are untouched. + Necessary for letting package identifiers be highlighted within + *Fuzzy Completions* in cases like "sb:with- C-c M-i." + + (fuzzy-completion-set): Taking out most code to become new + function FUZZY-CREATE-COMPLETION-SET. + + (fuzzy-create-completion-set): Doing all the hard work. Crux of + this changeset. so to speak. Largly rewritten to accomodate all + different cases of PARSE-COMPLETION-ARGUMENT. + + (fuzzy-find-matching-symbols, fuzzy-find-matching-packages): + Accomodating to new datatstructure FUZZY-MATCHING. Adapting + docstring accordingly. + + * swank-backend.lisp: Export WITH-STRUCT. + + * swank.lisp (eval-for-emacs, fuzzy-completions): + Various trivia like fixing spelling and indentation. + 2007-03-01 Tobias C. Rittweiler tcr@freebits.de
* slime.el (slime-fuzzy-highlight-current-completion): Fix @@ -5,7 +50,7 @@ completion in the *Fuzzy Completion* buffer be highlighted one char too far.
-2007-02-26 Tobias C. Rittweiler tcr@freebits.de +07-02-26 Tobias C. Rittweiler tcr@freebits.de
* swank.lisp: Cleanup of parts of the fuzzy completion code. Additionally a couple of enhancements. As follows: --- ../slime-hacked-1/swank.lisp 2007-03-03 15:05:27.000000000 +0100 +++ swank.lisp 2007-03-03 15:45:08.000000000 +0100 @@ -2472,7 +2472,7 @@ *package*))
(defun eval-for-emacs (form buffer-package id) - "Bind *BUFFER-PACKAGE* BUFFER-PACKAGE and evaluate FORM. + "Bind *BUFFER-PACKAGE* to BUFFER-PACKAGE and evaluate FORM. Return the result to the continuation ID. Errors are trapped and invoke our debugger." (call-with-debugger-hook @@ -3279,25 +3279,43 @@ collect (package-name package) append (package-nicknames package))))))
+;; PARSE-COMPLETION-ARGUMENTS return table: +;; +;; user behaviour | NAME | PACKAGE-NAME | PACKAGE +;; ----------------+--------+--------------+----------------------------------- +;; asdf [tab] | "asdf" | NIL | #<PACKAGE "DEFAULT-PACKAGE-NAME"> +;; | | | or *BUFFER-PACKAGE* +;; asdf: [tab] | "" | "asdf" | #<PACKAGE "ASDF"> +;; | | | +;; asdf:foo [tab] | "foo" | "asdf" | #<PACKAGE "ASDF"> +;; | | | +;; as:fo [tab] | "fo" | "as" | NIL +;; | | | +;; : [tab] | "" | "" | #<PACKAGE "KEYWORD"> +;; | | | +;; :foo [tab] | "foo" | "" | #<PACKAGE "KEYWORD"> +;; (defun parse-completion-arguments (string default-package-name) "Parse STRING as a symbol designator. Return these values: SYMBOL-NAME PACKAGE-NAME, or nil if the designator does not include an explicit package. - PACKAGE, the package to complete in + PACKAGE, generally the package to complete in. (However, if PACKAGE-NAME is + NIL, return the respective package of DEFAULT-PACKAGE-NAME instead; + if PACKAGE is non-NIL but a package cannot be found under that name, + return NIL.) INTERNAL-P, if the symbol is qualified with `::'." (multiple-value-bind (name package-name internal-p) (tokenize-symbol string) - (let ((package (carefully-find-package package-name default-package-name))) - (values name package-name package internal-p)))) + (if package-name + (let ((package (guess-package (if (equal package-name "") + "KEYWORD" + package-name)))) + (values name package-name package internal-p)) + (let ((package (guess-package default-package-name))) + (values name package-name (or package *buffer-package*) internal-p)) + )))
-(defun carefully-find-package (name default-package-name) - "Find the package with name NAME, or DEFAULT-PACKAGE-NAME, or the -*buffer-package*. NAME and DEFAULT-PACKAGE-NAME can be nil." - (let ((string (cond ((equal name "") "KEYWORD") - (t (or name default-package-name))))) - (or (and string (guess-package string)) - *buffer-package*)))
;;;;; Format completion results ;;; @@ -3465,9 +3483,13 @@
The result is a list of completion objects, where a completion object is: + (COMPLETED-STRING SCORE (&rest CHUNKS) FLAGS) + where a CHUNK is a description of a matched substring: + (OFFSET SUBSTRING) + and FLAGS is a list of keywords describing properties of the symbol (see CLASSIFY-SYMBOL).
@@ -3492,31 +3514,49 @@ ;; but then the network serialization were slower by handling arrays. ;; Instead we limit the number of completions that is transferred ;; (the limit is set from emacs). - (coerce (fuzzy-completion-set string default-package-name - :limit limit :time-limit-in-msec time-limit-in-msec) + (coerce (fuzzy-completion-set string default-package-name :limit limit + :time-limit-in-msec time-limit-in-msec) 'list))
-(defun convert-fuzzy-completion-result (fuzzy-matching converter - internal-p package-name) +;;; A Fuzzy Matching -- Not to be confused with a fuzzy completion +;;; object that will be sent back to Emacs, as described above. + +(defstruct (fuzzy-matching (:conc-name fuzzy-matching.) + (:predicate fuzzy-matching-p) + (:constructor %make-fuzzy-matching)) + symbol ; The symbol that has been found to match. + score ; the higher the better symbol is a match. + package-chunks ; Chunks pertaining to the package identifier of the symbol. + symbol-chunks) ; Chunks pertaining to the symbol's name. + +(defun make-fuzzy-matching (symbol score package-chunks symbol-chunks) + (%make-fuzzy-matching :symbol symbol :score score + :package-chunks package-chunks + :symbol-chunks symbol-chunks)) + + +(defun fuzzy-convert-matching-for-emacs (fuzzy-matching converter + internal-p package-name) "Converts a result from the fuzzy completion core into something that emacs is expecting. Converts symbols to strings, fixes case issues, and adds information describing if the symbol is :bound, :fbound, a :class, a :macro, a :generic-function, a :special-operator, or a :package." - (destructuring-bind (symbol score chunks) fuzzy-matching + (with-struct (fuzzy-matching. symbol score package-chunks symbol-chunks) fuzzy-matching (multiple-value-bind (name added-length) (format-completion-result (funcall (or converter #'identity) (symbol-name symbol)) internal-p package-name) (list name score - (mapcar #'(lambda (chunk) - ;; fix up chunk positions to account for possible - ;; added package identifier - (list (+ added-length (first chunk)) - (second chunk))) - chunks) + (append package-chunks + (mapcar #'(lambda (chunk) + ;; fix up chunk positions to account for possible + ;; added package identifier. + (let ((offset (first chunk)) (string (second chunk))) + (list (+ added-length offset) string))) + symbol-chunks)) (classify-symbol symbol)))))
(defun classify-symbol (symbol) @@ -3538,50 +3578,107 @@ (push :generic-function result)) result))
+ (defun fuzzy-completion-set (string default-package-name &key limit time-limit-in-msec) "Prepares list of completion objects, sorted by SCORE, of fuzzy completions of STRING in DEFAULT-PACKAGE-NAME. If LIMIT is set, only the top LIMIT results will be returned." - (declare (type (or null (integer 0 #.(1- most-positive-fixnum))) limit time-limit-in-msec)) - (multiple-value-bind (name package-name package internal-p) - (parse-completion-arguments string default-package-name) - (flet ((convert (vector &optional converter) - (when vector - (map-into vector - #'(lambda (fuzzy-matching) - (convert-fuzzy-completion-result fuzzy-matching converter - internal-p package-name)) - vector)))) - (let* ((symbols (and package - (fuzzy-find-matching-symbols name package - :time-limit-in-msec time-limit-in-msec - :external-only (and (not internal-p) - package-name)))) - (packages (and (not package-name) - (fuzzy-find-matching-packages name))) - (results)) - (convert symbols (completion-output-symbol-converter string)) - (convert packages #'(lambda (package-name) - (let ((converter (completion-output-package-converter string))) - ;; Present packages with a trailing colon for maximum convenience! - (concatenate 'string (funcall converter package-name) ":")))) - ;; Sort alphabetically before sorting by score. (Especially useful when - ;; STRING is empty, and SYMBOLS is a list of all possible completions.) - (setf results (sort (concatenate 'vector symbols packages) #'string-lessp :key #'first)) - (setf results (stable-sort results #'> :key #'second)) + (check-type (values limit time-limit-in-msec) + (or null (integer 0 #.(1- most-positive-fixnum)))) + (let* ((completion-set (fuzzy-create-completion-set string default-package-name + time-limit-in-msec))) (when (and limit (> limit 0) - (< limit (length results))) - (if (array-has-fill-pointer-p results) - (setf (fill-pointer results) limit) - (setf results (make-array limit :displaced-to results)))) - results)))) + (< limit (length completion-set))) + (if (array-has-fill-pointer-p completion-set) + (setf (fill-pointer completion-set) limit) + (setf completion-set (make-array limit :displaced-to completion-set)))) + completion-set)) + + +(defun fuzzy-create-completion-set (string default-package-name time-limit-in-msec) + "Does all the hard work for FUZZY-COMPLETION-SET." + (multiple-value-bind (parsed-name parsed-package-name package internal-p) + (parse-completion-arguments string default-package-name) + (flet ((convert (matchings package-name &optional converter) + ;; Converts MATCHINGS to completion objects for Emacs. + ;; PACKAGE-NAME is the package identifier that's used as prefix + ;; during formatting. If NIL, the identifier is omitted. + (map-into matchings + #'(lambda (m) + (fuzzy-convert-matching-for-emacs m converter + internal-p + package-name)) + matchings)) + (fix-up (matchings parent-package-matching) + ;; The components of each matching in MATCHINGS have been computed + ;; relative to PARENT-PACKAGE-MATCHING. Make them absolute. + (let* ((p parent-package-matching) + (p.score (fuzzy-matching.score p)) + (p.chunks (fuzzy-matching.package-chunks p))) + (map-into matchings + #'(lambda (m) + (let ((m.score (fuzzy-matching.score m))) + (setf (fuzzy-matching.package-chunks m) p.chunks) + (setf (fuzzy-matching.score m) + (if (string= parsed-name "") + ;; (make packages be sorted before their symbol + ;; matchings while preserving over all orderness + ;; among different symbols in different packages) + (/ p.score 100) + (+ p.score m.score))) + m)) + matchings))) + (find-matchings (designator package) + (fuzzy-find-matching-symbols designator package + :time-limit-in-msec time-limit-in-msec + :external-only (not internal-p)))) + (let ((symbol-normalizer (completion-output-symbol-converter string)) + (package-normalizer #'(lambda (package-name) + (let ((converter (completion-output-package-converter string))) + ;; Present packages with a trailing colon for maximum convenience! + (concatenate 'string (funcall converter package-name) ":")))) + (symbols) (packages) (results)) + (cond ((not parsed-package-name) ; STRING = "asd" + ;; We don't know if user is searching for a package or a symbol + ;; within his current package. So we try to find either. + (setf symbols (find-matchings parsed-name package) + symbols (convert symbols nil symbol-normalizer) + packages (fuzzy-find-matching-packages parsed-name) + packages (convert packages nil package-normalizer))) + ((string= parsed-package-name "") ; STRING = ":" or ":foo" + (setf symbols (find-matchings parsed-name package) + symbols (convert symbols "" symbol-normalizer))) + (t ; STRING= "asdf:" or "asdf:foo" + ;; Find fuzzy matchings of the denoted package identifier part. + ;; After that find matchings for the denoted symbol identifier + ;; relative to all those packages found. + (loop + with found-packages = (fuzzy-find-matching-packages parsed-package-name) + for package-matching across found-packages + do + (let* ((pkgsym (fuzzy-matching.symbol package-matching)) + (package-name (symbol-name pkgsym)) + (package-name (funcall symbol-normalizer package-name)) + (matchings (find-matchings parsed-name (find-package pkgsym)))) + (setf matchings (fix-up matchings package-matching)) + (setf matchings (convert matchings package-name symbol-normalizer)) + (setf symbols (concatenate 'vector symbols matchings))) + finally ; CONVERT is destructive. So we have to do this at last. + (when (string= parsed-name "") + (setf packages (convert found-packages nil package-normalizer)))))) + ;; Sort alphabetically before sorting by score. (Especially useful when + ;; PARSED-NAME is empty, and all possible completions are to be returned.) + (setf results (concatenate 'vector symbols packages)) + (setf results (sort results #'string-lessp :key #'first)) + (setf results (stable-sort results #'> :key #'second)) + results)))) +
(defun fuzzy-find-matching-symbols (string package &key external-only time-limit-in-msec) - "Returns a vector of fuzzy matchings (that is a list of the symbol in -PACKAGE that's matching STRING, its score, and a list of its completion -chunks), using the fuzzy completion algorithm. If EXTERNAL-ONLY is true, -only external symbols are considered." + "Returns a vector of fuzzy matchings for matching symbols in PACKAGE, +using the fuzzy completion algorithm. If EXTERNAL-ONLY is true, only +external symbols are considered." (let ((completions (make-array 256 :adjustable t :fill-pointer 0)) (converter (completion-output-symbol-converter string)) (time-limit (if time-limit-in-msec @@ -3593,7 +3690,7 @@ (declare (type function converter)) (flet ((time-exhausted-p () (and (not (zerop time-limit)) - (zerop (mod count 256)) ; ease up on calling get-universal-time like crazy + (zerop (mod count 256)) ; ease up on calling get-universal-time like crazy (incf count) (>= (- (get-universal-time) utime-at-start) time-limit))) (perform-fuzzy-match (string symbol-name) @@ -3604,17 +3701,19 @@ (do-symbols* (symbol package) (when (time-exhausted-p) (return-from loop)) (when (or (not external-only) (symbol-external-p symbol package)) - (if (string= "" string) - (vector-push-extend (list symbol 0.0 (list (list 0 ""))) completions) + (if (string= "" string) + (vector-push-extend (make-fuzzy-matching symbol 0.0 '() '()) + completions) ; create vanilla matching. (multiple-value-bind (match-result score) (perform-fuzzy-match string (symbol-name symbol)) (when match-result - (vector-push-extend (list symbol score match-result) completions))))))))))) + (vector-push-extend (make-fuzzy-matching symbol score '() match-result) + completions))))))))))) +
(defun fuzzy-find-matching-packages (name) - "Returns a vector of relevant fuzzy matchings (that is a list -consisting of a symbol representing the package that matches NAME, -its score, and its completions chunks.)" + "Returns a vector of fuzzy matchings for each package that +is similiar to NAME." (let ((converter (completion-output-package-converter name)) (completions (make-array 32 :adjustable t :fill-pointer 0))) (declare ;;(optimize (speed 3)) @@ -3627,14 +3726,12 @@ for (result score) = (multiple-value-list ; too invasive. (compute-highest-scoring-completion name converted-name)) - ;; We return a symbol that represents the package, a) to make - ;; the type of the returned value consistent with the one of - ;; FUZZY-FIND-MATCHING-SYMBOLS, and b) to be able to call - ;; CLASSIFY-SYMBOL upon it later on. - when result do - (vector-push-extend (list package-symbol score result) completions)) + when result do (vector-push-extend + (make-fuzzy-matching package-symbol score result '()) + completions)) completions))
+ (defslimefun fuzzy-completion-selected (original-string completion) "This function is called by Slime when a fuzzy completion is selected by the user. It is for future expansion to make @@ -3648,6 +3745,7 @@ (declare (ignore original-string completion)) nil)
+ ;;;;; Fuzzy completion core
(defparameter *fuzzy-recursion-soft-limit* 30 @@ -3759,6 +3857,7 @@ (push rev-chunks *all-chunks*) rev-chunks))))
+ ;;;;; Fuzzy completion scoring
(defparameter *fuzzy-completion-symbol-prefixes* "*+-%&?<" --- ../slime-hacked-1/swank-backend.lisp 2007-02-25 19:19:55.000000000 +0100 +++ swank-backend.lisp 2007-02-28 20:43:08.000000000 +0100 @@ -39,6 +39,7 @@ #:label-value-line #:label-value-line* #:type-for-emacs + #:with-struct ))
(defpackage :swank-mop