(defpackage :slynk-apropos (:use #:cl #:slynk-api) (:export #:apropos-list-for-emacs #:*preferred-apropos-matcher*)) (in-package :slynk-apropos) (defparameter *preferred-apropos-matcher* 'make-cl-ppcre-matcher "Preferred matcher for apropos searches. Value is a function of three arguments , PATTERN, CASE-SENSITIVE and SYMBOL-NAME-FN that should return a function, called MATCHER of one argument, a SYMBOL. MATCHER should return non-nil if PATTERN somehow matches the result of applying SYMBOL-NAME-FN to SYMBOL, according to CASE-SENSITIVE. The non-nil return value can be a list of integer or a list of lists of integers.") (defslyfun apropos-list-for-emacs (pattern &optional external-only case-sensitive package) "Make an apropos search for Emacs. The result is a list of property lists." (let ((package (if package (or (parse-package package) (error "No such package: ~S" package))))) ;; The MAPCAN will filter all uninteresting symbols, i.e. those ;; who cannot be meaningfully described. ;; ;; *BUFFER-PACKAGE* is exceptionally set so that the symbol ;; listing will only omit package qualifier iff the user specified ;; PACKAGE. (let* ((*buffer-package* (or package slynk::*slynk-io-package*)) (matcher (funcall *preferred-apropos-matcher* pattern case-sensitive)) (seen (make-hash-table)) result) (do-all-symbols (sym) (let ((external (symbol-external-p sym))) (multiple-value-bind (bounds score) (and (symbol-package sym) ; see github#266 (funcall matcher (if package (string sym) (concatenate 'string (package-name (symbol-package sym)) (if external ":" "::") (symbol-name sym))))) (unless (gethash sym seen) (when bounds (unless (or (and external-only (not external)) (and package (not (eq package (symbol-package sym))))) (push `(,sym :bounds ,bounds ,@(and score `(:flex-score ,score)) :external-p ,external) result))) (setf (gethash sym seen) t))))) (loop for (symbol . extra) in (sort result (lambda (x y) (let ((scorex (getf (cdr x) :flex-score)) (scorey (getf (cdr y) :flex-score))) (if (and scorex scorey) (> scorex scorey) (present-symbol-before-p (car x) (car y)))))) for short = (briefly-describe-symbol-for-emacs symbol (getf extra :external-p)) for score = (getf extra :flex-score) when score do (setf (getf extra :flex-score) (format nil "~2$%" (* 100 score))) do (remf extra :external-p) when short collect (append short extra))))) (defun briefly-describe-symbol-for-emacs (symbol external-p) "Return a property list describing SYMBOL. Like `describe-symbol-for-emacs' but with at most one line per item." (flet ((first-line (string) (let ((pos (position #\newline string))) (if (null pos) string (subseq string 0 pos))))) (let ((desc (map-if #'stringp #'first-line (slynk-backend:describe-symbol-for-emacs symbol)))) (if desc `(:designator ,(list (symbol-name symbol) (let ((package (symbol-package symbol))) (and package (package-name package))) external-p) ,@desc ,@(let ((arglist (and (fboundp symbol) (slynk-backend:arglist symbol)))) (when (and arglist (not (eq arglist :not-available))) `(:arglist ,(princ-to-string arglist))))))))) (defun present-symbol-before-p (x y) "Return true if X belongs before Y in a printed summary of symbols. Sorted alphabetically by package name and then symbol name, except that symbols accessible in the current package go first." (declare (type symbol x y)) (flet ((accessible (s) ;; Test breaks on NIL for package that does not inherit it (eq (find-symbol (symbol-name s) *buffer-package*) s))) (let ((ax (accessible x)) (ay (accessible y))) (cond ((and ax ay) (string< (symbol-name x) (symbol-name y))) (ax t) (ay nil) (t (let ((px (symbol-package x)) (py (symbol-package y))) (if (eq px py) (string< (symbol-name x) (symbol-name y)) (string< (package-name px) (package-name py))))))))) (defun make-cl-ppcre-matcher (pattern case-sensitive) (if (not (every #'alpha-char-p pattern)) (cond ((find-package :cl-ppcre) (background-message "Using CL-PPCRE for apropos on regexp \"~a\"" pattern) (let ((matcher (funcall (read-from-string "cl-ppcre:create-scanner") pattern :case-insensitive-mode (not case-sensitive)))) (lambda (symbol-name) (multiple-value-bind (beg end) (funcall (read-from-string "cl-ppcre:scan") matcher symbol-name) (when beg `((,beg ,end))))))) (t (background-message "Using plain apropos. Load CL-PPCRE to enable regexps") (make-plain-matcher pattern case-sensitive))) (make-plain-matcher pattern case-sensitive))) (defun make-plain-matcher (pattern case-sensitive) (let ((chr= (if case-sensitive #'char= #'char-equal))) (lambda (symbol-name) (let ((beg (search pattern symbol-name :test chr=))) (when beg `((,beg ,(+ beg (length pattern))))))))) (defun make-flex-matcher (pattern case-sensitive) (if (zerop (length pattern)) (make-plain-matcher pattern case-sensitive) (let ((chr= (if case-sensitive #'char= #'char-equal))) (lambda (symbol-name) (slynk-completion:flex-matches pattern symbol-name chr=)))))