;;; -*- Mode: LISP; Syntax: COMMON-LISP; Package: LW-ADD-ONS; Base: 10 -*- ;;; $Header: /usr/local/cvsrep/lw-add-ons/completions.lisp,v 1.14 2015/03/06 12:54:25 edi Exp $ ;;; Copyright (c) 2005-2015, Dr. Edmund Weitz. All rights reserved. ;;; Redistribution and use in source and binary forms, with or without ;;; modification, are permitted provided that the following conditions ;;; are met: ;;; * Redistributions of source code must retain the above copyright ;;; notice, this list of conditions and the following disclaimer. ;;; * Redistributions in binary form must reproduce the above ;;; copyright notice, this list of conditions and the following ;;; disclaimer in the documentation and/or other materials ;;; provided with the distribution. ;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED ;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY ;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE ;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, ;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING ;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ;;; This code copied almost verbatim from SLIME, see ;;; <http://common-lisp.net/project/slime/> (in-package :lw-add-ons) (defun compound-prefix-match (prefix target) "Return true if PREFIX is a compound-prefix of TARGET. Viewing each of PREFIX and TARGET as a series of substrings delimited by hyphens, if each substring of PREFIX is a prefix of the corresponding substring in TARGET then we call PREFIX a compound-prefix of TARGET. Examples: \(compound-prefix-match \"foo\" \"foobar\") => t \(compound-prefix-match \"m--b\" \"multiple-value-bind\") => t \(compound-prefix-match \"m-v-c\" \"multiple-value-bind\") => NIL" (declare (type simple-string prefix target)) (loop for ch across prefix with tpos = 0 always (and (< tpos (length target)) (if (char= ch #\-) (setf tpos (position #\- target :start tpos)) (char= ch (aref target tpos)))) do (incf tpos))) ;; FIXME: deal with #\| etc. hard to do portably. (defun tokenize-symbol (string) (let ((package (let ((pos (position #\: string))) (if pos (subseq string 0 pos) nil))) (symbol (let ((pos (position #\: string :from-end t))) (if pos (subseq string (1+ pos)) string))) (internp (search "::" string))) (values symbol package internp))) (defun parse-package (string) "Find the package named STRING. Return the package or nil." (multiple-value-bind (name pos) (if (zerop (length string)) (values :|| 0) (let ((*package* +keyword-package+)) (ignore-errors* (read-from-string string)))) (if (and (or (keywordp name) (stringp name)) (= (length string) pos)) (find-package name)))) (defun guess-package-from-string (name &optional (default-package *package*)) (or (and name (or (parse-package name) (find-package (string-upcase name)) (parse-package (substitute #\- #\! name)))) default-package)) (defun carefully-find-package (name default-package-name) "Find the package with name NAME, or DEFAULT-PACKAGE-NAME, or the CL-USER package. NAME and DEFAULT-PACKAGE-NAME can be nil." (let ((string (cond ((equal name "") "KEYWORD") (t (or name default-package-name))))) (if string (guess-package-from-string string nil) +cl-user-package+))) (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 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)))) (defun determine-case (string) "Return two booleans LOWER and UPPER indicating whether STRING contains lower or upper case characters." (values (some #'lower-case-p string) (some #'upper-case-p string))) (defun output-case-converter (input) "Return a function to case convert strings for output. INPUT is used to guess the preferred case." (ecase (readtable-case *readtable*) (:upcase (if (some #'lower-case-p input) #'string-downcase #'identity)) (:invert (lambda (output) (multiple-value-bind (lower upper) (determine-case output) (cond ((and lower upper) output) (lower (string-upcase output)) (upper (string-downcase output)) (t output))))) (:downcase (if (some #'upper-case-p input) #'string-upcase #'identity)) (:preserve #'identity))) (defun symbol-external-p (symbol &optional (package (symbol-package symbol))) "True if SYMBOL is external in PACKAGE. If PACKAGE is not specified, the home package of SYMBOL is used." (unless package (setq package (symbol-package symbol))) (when package (multiple-value-bind (_ status) (find-symbol (symbol-name symbol) package) (declare (ignore _)) (eq status :external)))) (defun find-matching-symbols (string package external test) "Return a list of symbols in PACKAGE matching STRING. TEST is called with two strings. If EXTERNAL is true, only external symbols are returned." (let ((completions '()) (converter (output-case-converter string))) (flet ((symbol-matches-p (symbol) (and (or (not external) (symbol-external-p symbol package)) (funcall test string (funcall converter (symbol-name symbol)))))) (do-symbols (symbol package) (when (symbol-matches-p symbol) (push symbol completions)))) (delete-duplicates completions))) (defun find-matching-packages (name matcher) "Return a list of package names matching NAME using the fuzzy completion algorithm." (let ((to-match (string-upcase name))) (remove-if-not (lambda (x) (funcall matcher to-match x)) (mapcar (lambda (pkgname) (concatenate 'string pkgname ":")) (loop for package in (list-all-packages) collect (package-name package) append (package-nicknames package)))))) (defun format-completion-result (string internal-p package-name) (let ((prefix (cond (internal-p (format nil "~A::" package-name)) (package-name (format nil "~A:" package-name)) (t "")))) (values (concatenate 'string prefix string) (length prefix)))) (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) (format-completion-result string internal-p package-name)) (sort strings #'string<))) (defun completion-set (string default-package-name matchp) "Return the set of completion-candidates as strings." (multiple-value-bind (name package-name package internal-p) (parse-completion-arguments string default-package-name) (let* ((symbols (and package (find-matching-symbols name package (and (not internal-p) package-name) matchp))) (packs (and (not package-name) (find-matching-packages name matchp))) (converter (output-case-converter name)) (strings (mapcar converter (nconc (mapcar #'symbol-name symbols) packs)))) (format-completion-set strings internal-p package-name)))) (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 transpose-lists (lists) "Turn a list-of-lists on its side. If the rows are of unequal length, truncate uniformly to the shortest. For example: \(transpose-lists '((ONE TWO THREE) (1 2))) => ((ONE 1) (TWO 2))" (cond ((null lists) '()) ((some #'null lists) '()) (t (cons (mapcar #'car lists) (transpose-lists (mapcar #'cdr lists)))))) (defun untokenize-completion (tokens) (format nil "~{~A~^-~}" tokens)) (defun tokenize-completion (string) "Return all substrings of STRING delimited by #\-." (loop with end for start = 0 then (1+ end) until (> start (length string)) do (setq end (or (position #\- string :start start) (length string))) collect (subseq string start end))) (defun longest-completion (completions) "Return the longest prefix for all COMPLETIONS. COMPLETIONS is a list of strings." (untokenize-completion (mapcar #'longest-common-prefix (transpose-lists (mapcar #'tokenize-completion completions))))) (defun completions (string default-package-name) "Return a list of completions for a symbol designator STRING. The result is the list \(COMPLETION-SET COMPLETED-PREFIX). COMPLETION-SET is the list of all matching completions, and COMPLETED-PREFIX is the best (partial) completion of the input string. If STRING is package qualified the result list will also be qualified. If string is non-qualified the result strings are also not qualified and are considered relative to DEFAULT-PACKAGE-NAME. The way symbols are matched depends on the symbol designator's format. The cases are as follows: FOO - Symbols with matching prefix and accessible in the buffer package. PKG:FOO - Symbols with matching prefix and external in package PKG. PKG::FOO - Symbols with matching prefix and accessible in package PKG." (let ((completion-set (completion-set string default-package-name *completion-match-function*))) (values completion-set (longest-completion completion-set))))