(require 'slime) (require 'cl-lib) (define-slime-contrib slime-parse "Utility contrib containg functions to parse forms in a buffer." (:authors "Matthias Koeppe " "Tobias C. Rittweiler ") (:license "GPL")) (defun slime-parse-form-until (limit form-suffix) "Parses form from point to `limit'." ;; For performance reasons, this function does not use recursion. (let ((todo (list (point))) ; stack of positions (sexps) ; stack of expressions (cursexp) (curpos) (depth 1)) ; This function must be called from the ; start of the sexp to be parsed. (while (and (setq curpos (pop todo)) (progn (goto-char curpos) ;; (Here we also move over suppressed ;; reader-conditionalized code! Important so CL-side ;; of autodoc won't see that garbage.) (ignore-errors (slime-forward-cruft)) (< (point) limit))) (setq cursexp (pop sexps)) (cond ;; End of an sexp? ((or (looking-at "\\s)") (eolp)) (cl-decf depth) (push (nreverse cursexp) (car sexps))) ;; Start of a new sexp? ((looking-at "\\s'*@*\\s(") (let ((subpt (match-end 0))) (ignore-errors (forward-sexp) ;; (In case of error, we're at an incomplete sexp, and ;; nothing's left todo after it.) (push (point) todo)) (push cursexp sexps) (push subpt todo) ; to descend into new sexp (push nil sexps) (cl-incf depth))) ;; In mid of an sexp.. (t (let ((pt1 (point)) (pt2 (condition-case e (progn (forward-sexp) (point)) (scan-error (cl-fourth e))))) ; end of sexp (push (buffer-substring-no-properties pt1 pt2) cursexp) (push pt2 todo) (push cursexp sexps))))) (when sexps (setf (car sexps) (cl-nreconc form-suffix (car sexps))) (while (> depth 1) (push (nreverse (pop sexps)) (car sexps)) (cl-decf depth)) (nreverse (car sexps))))) (defun slime-compare-char-syntax (get-char-fn syntax &optional unescaped) "Returns t if the character that `get-char-fn' yields has characer syntax of `syntax'. If `unescaped' is true, it's ensured that the character is not escaped." (let ((char (funcall get-char-fn (point))) (char-before (funcall get-char-fn (1- (point))))) (if (and char (eq (char-syntax char) (aref syntax 0))) (if unescaped (or (null char-before) (not (eq (char-syntax char-before) ?\\))) t) nil))) (defconst slime-cursor-marker 'swank::%cursor-marker%) (defun slime-parse-form-upto-point (&optional max-levels) (save-restriction ;; Don't parse more than 500 lines before point, so we don't spend ;; too much time. NB. Make sure to go to beginning of line, and ;; not possibly anywhere inside comments or strings. (narrow-to-region (line-beginning-position -500) (point-max)) (save-excursion (let ((suffix (list slime-cursor-marker))) (cond ((slime-compare-char-syntax #'char-after "(" t) ;; We're at the start of some expression, so make sure ;; that SWANK::%CURSOR-MARKER% will come after that ;; expression. If the expression is not balanced, make ;; still sure that the marker does *not* come directly ;; after the preceding expression. (or (ignore-errors (forward-sexp) t) (push "" suffix))) ((or (bolp) (slime-compare-char-syntax #'char-before " " t)) ;; We're after some expression, so we have to make sure ;; that %CURSOR-MARKER% does *not* come directly after ;; that expression. (push "" suffix)) ((slime-compare-char-syntax #'char-before "(" t) ;; We're directly after an opening parenthesis, so we ;; have to make sure that something comes before ;; %CURSOR-MARKER%. (push "" suffix)) (t ;; We're at a symbol, so make sure we get the whole symbol. (slime-end-of-symbol))) (let ((pt (point))) (ignore-errors (up-list (if max-levels (- max-levels) -5))) (ignore-errors (down-list)) (slime-parse-form-until pt suffix)))))) (require 'bytecomp) (mapc (lambda (sym) (cond ((fboundp sym) (unless (byte-code-function-p (symbol-function sym)) (byte-compile sym))) (t (error "%S is not fbound" sym)))) '(slime-parse-form-upto-point slime-parse-form-until slime-compare-char-syntax)) ;;;; Test cases (defun slime-extract-context () "Parse the context for the symbol at point. Nil is returned if there's no symbol at point. Otherwise we detect the following cases (the . shows the point position): (defun n.ame (...) ...) -> (:defun name) (defun (setf n.ame) (...) ...) -> (:defun (setf name)) (defmethod n.ame (...) ...) -> (:defmethod name (...)) (defun ... (...) (labels ((n.ame (...) -> (:labels (:defun ...) name) (defun ... (...) (flet ((n.ame (...) -> (:flet (:defun ...) name) (defun ... (...) ... (n.ame ...) ...) -> (:call (:defun ...) name) (defun ... (...) ... (setf (n.ame ...) -> (:call (:defun ...) (setf name)) (defmacro n.ame (...) ...) -> (:defmacro name) (defsetf n.ame (...) ...) -> (:defsetf name) (define-setf-expander n.ame (...) ...) -> (:define-setf-expander name) (define-modify-macro n.ame (...) ...) -> (:define-modify-macro name) (define-compiler-macro n.ame (...) ...) -> (:define-compiler-macro name) (defvar n.ame (...) ...) -> (:defvar name) (defparameter n.ame ...) -> (:defparameter name) (defconstant n.ame ...) -> (:defconstant name) (defclass n.ame ...) -> (:defclass name) (defstruct n.ame ...) -> (:defstruct name) (defpackage n.ame ...) -> (:defpackage name) For other contexts we return the symbol at point." (let ((name (slime-symbol-at-point))) (if name (let ((symbol (read name))) (or (progn ;;ignore-errors (slime-parse-context symbol)) symbol))))) (defun slime-parse-context (name) (save-excursion (cond ((slime-in-expression-p '(defun *)) `(:defun ,name)) ((slime-in-expression-p '(defmacro *)) `(:defmacro ,name)) ((slime-in-expression-p '(defgeneric *)) `(:defgeneric ,name)) ((slime-in-expression-p '(setf *)) ;;a setf-definition, but which? (backward-up-list 1) (slime-parse-context `(setf ,name))) ((slime-in-expression-p '(defmethod *)) (unless (looking-at "\\s ") (forward-sexp 1)) ; skip over the methodname (let (qualifiers arglist) (cl-loop for e = (read (current-buffer)) until (listp e) do (push e qualifiers) finally (setq arglist e)) `(:defmethod ,name ,@qualifiers ,(slime-arglist-specializers arglist)))) ((and (symbolp name) (slime-in-expression-p `(,name))) ;; looks like a regular call (let ((toplevel (ignore-errors (slime-parse-toplevel-form)))) (cond ((slime-in-expression-p `(setf (*))) ;a setf-call (if toplevel `(:call ,toplevel (setf ,name)) `(setf ,name))) ((not toplevel) name) ((slime-in-expression-p `(labels ((*)))) `(:labels ,toplevel ,name)) ((slime-in-expression-p `(flet ((*)))) `(:flet ,toplevel ,name)) (t `(:call ,toplevel ,name))))) ((slime-in-expression-p '(define-compiler-macro *)) `(:define-compiler-macro ,name)) ((slime-in-expression-p '(define-modify-macro *)) `(:define-modify-macro ,name)) ((slime-in-expression-p '(define-setf-expander *)) `(:define-setf-expander ,name)) ((slime-in-expression-p '(defsetf *)) `(:defsetf ,name)) ((slime-in-expression-p '(defvar *)) `(:defvar ,name)) ((slime-in-expression-p '(defparameter *)) `(:defparameter ,name)) ((slime-in-expression-p '(defconstant *)) `(:defconstant ,name)) ((slime-in-expression-p '(defclass *)) `(:defclass ,name)) ((slime-in-expression-p '(defpackage *)) `(:defpackage ,name)) ((slime-in-expression-p '(defstruct *)) `(:defstruct ,(if (consp name) (car name) name))) (t name)))) (defun slime-in-expression-p (pattern) "A helper function to determine the current context. The pattern can have the form: pattern ::= () ;matches always | (*) ;matches inside a list | ( ) ;matches if the first element in ; the current list is and ; if matches. | (()) ;matches if we are in a nested list." (save-excursion (let ((path (reverse (slime-pattern-path pattern)))) (cl-loop for p in path always (ignore-errors (cl-etypecase p (symbol (slime-beginning-of-list) (eq (read (current-buffer)) p)) (number (backward-up-list p) t))))))) (defun slime-pattern-path (pattern) ;; Compute the path to the * in the pattern to make matching ;; easier. The path is a list of symbols and numbers. A number ;; means "(down-list )" and a symbol "(look-at )") (if (null pattern) '() (cl-etypecase (car pattern) ((member *) '()) (symbol (cons (car pattern) (slime-pattern-path (cdr pattern)))) (cons (cons 1 (slime-pattern-path (car pattern))))))) (defun slime-beginning-of-list (&optional up) "Move backward to the beginning of the current expression. Point is placed before the first expression in the list." (backward-up-list (or up 1)) (down-list 1) (skip-syntax-forward " ")) (defun slime-end-of-list (&optional up) (backward-up-list (or up 1)) (forward-list 1) (down-list -1)) (defun slime-parse-toplevel-form () (ignore-errors ; (foo) (save-excursion (goto-char (car (slime-region-for-defun-at-point))) (down-list 1) (forward-sexp 1) (slime-parse-context (read (current-buffer)))))) (defun slime-arglist-specializers (arglist) (cond ((or (null arglist) (member (cl-first arglist) '(&optional &key &rest &aux))) (list)) ((consp (cl-first arglist)) (cons (cl-second (cl-first arglist)) (slime-arglist-specializers (cl-rest arglist)))) (t (cons 't (slime-arglist-specializers (cl-rest arglist)))))) (defun slime-definition-at-point (&optional only-functional) "Return object corresponding to the definition at point." (let ((toplevel (slime-parse-toplevel-form))) (if (or (symbolp toplevel) (and only-functional (not (member (car toplevel) '(:defun :defgeneric :defmethod :defmacro :define-compiler-macro))))) (error "Not in a definition") (slime-dcase toplevel (((:defun :defgeneric) symbol) (format "#'%s" symbol)) (((:defmacro :define-modify-macro) symbol) (format "(macro-function '%s)" symbol)) ((:define-compiler-macro symbol) (format "(compiler-macro-function '%s)" symbol)) ((:defmethod symbol &rest args) (declare (ignore args)) (format "#'%s" symbol)) (((:defparameter :defvar :defconstant) symbol) (format "'%s" symbol)) (((:defclass :defstruct) symbol) (format "(find-class '%s)" symbol)) ((:defpackage symbol) (format "(or (find-package '%s) (error \"Package %s not found\"))" symbol symbol)) (t (error "Not in a definition")))))) (defsubst slime-current-parser-state () ;; `syntax-ppss' does not save match data as it invokes ;; `beginning-of-defun' implicitly which does not save match ;; data. This issue has been reported to the Emacs maintainer on ;; Feb27. (syntax-ppss)) (defun slime-inside-string-p () (nth 3 (slime-current-parser-state))) (defun slime-inside-comment-p () (nth 4 (slime-current-parser-state))) (defun slime-inside-string-or-comment-p () (let ((state (slime-current-parser-state))) (or (nth 3 state) (nth 4 state)))) ;;; The following two functions can be handy when inspecting ;;; source-location while debugging `M-.'. ;;; (defun slime-current-tlf-number () "Return the current toplevel number." (interactive) (let ((original-pos (car (slime-region-for-defun-at-point))) (n 0)) (save-excursion ;; We use this and no repeated `beginning-of-defun's to get ;; reader conditionals right. (goto-char (point-min)) (while (progn (slime-forward-sexp) (< (point) original-pos)) (cl-incf n))) n)) ;;; This is similiar to `slime-enclosing-form-paths' in the ;;; `slime-parse' contrib except that this does not do any duck-tape ;;; parsing, and gets reader conditionals right. (defun slime-current-form-path () "Returns the path from the beginning of the current toplevel form to the atom at point, or nil if we're in front of a tlf." (interactive) (let ((source-path nil)) (save-excursion ;; Moving forward to get reader conditionals right. (cl-loop for inner-pos = (point) for outer-pos = (cl-nth-value 1 (slime-current-parser-state)) while outer-pos do (goto-char outer-pos) (unless (eq (char-before) ?#) ; when at #(...) continue. (forward-char) (let ((n 0)) (while (progn (slime-forward-sexp) (< (point) inner-pos)) (cl-incf n)) (push n source-path) (goto-char outer-pos))))) source-path)) (provide 'slime-parse)