(require 'slime)
|
|
(require 'cl-lib)
|
|
|
|
(define-slime-contrib slime-parse
|
|
"Utility contrib containg functions to parse forms in a buffer."
|
|
(:authors "Matthias Koeppe <mkoeppe@mail.math.uni-magdeburg.de>"
|
|
"Tobias C. Rittweiler <tcr@freebits.de>")
|
|
(: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
|
|
| (<symbol> <pattern>) ;matches if the first element in
|
|
; the current list is <symbol> and
|
|
; if <pattern> matches.
|
|
| ((<pattern>)) ;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 <n>)" and a symbol "(look-at <sym>)")
|
|
(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)
|