(defpackage :slynk
(:use :cl :slynk-backend :slynk-match :slynk-rpc)
(:export #:startup-multiprocessing
#:start-server
#:create-server
#:stop-server
#:restart-server
#:ed-in-emacs
#:inspect-in-emacs
#:print-indentation-lossage
#:invoke-sly-debugger
#:slynk-debugger-hook
#:emacs-inspect
#:authenticate-client
#:*loopback-interface*
#:*buffer-readtable*
#:process-requests)
(:export #:*communication-style*
#:*dont-close*
#:*fasl-pathname-function*
#:*log-events*
#:*log-output*
#:*configure-emacs-indentation*
#:*readtable-alist*
#:*global-debugger*
#:*sly-db-quit-restart*
#:*backtrace-printer-bindings*
#:*default-worker-thread-bindings*
#:*macroexpand-printer-bindings*
#:*slynk-pprint-bindings*
#:*string-elision-length*
#:*inspector-verbose*
#:*require-module*
#:*eval-for-emacs-wrappers*
#:*debugger-extra-options*
#:*globally-redirect-io*
#:*use-dedicated-output-stream*
#:*dedicated-output-stream-port*
#:*dedicated-output-stream-buffering*
#:debug-on-slynk-error
#:buffer-first-change
#:frame-source-location
#:gdb-initial-commands
#:restart-frame
#:sly-db-step
#:sly-db-break
#:sly-db-break-on-return
#:default-directory
#:set-default-directory
#:quit-lisp
#:eval-for-emacs
#:eval-in-emacs
#:y-or-n-p-in-emacs
#:*find-definitions-right-trim*
#:*find-definitions-left-trim*
#:*after-toggle-trace-hook*
#:*echo-number-alist*
#:*present-number-alist*))
(in-package :slynk)
(defconstant cl-package (find-package :cl)
"The COMMON-LISP package.")
(defconstant +keyword-package+ (find-package :keyword)
"The KEYWORD package.")
(defconstant default-server-port 4005
"The default TCP port for the server (when started manually).")
(defvar *slynk-debug-p* t
"When true, print extra debugging information.")
(defvar *m-x-sly-from-emacs* nil
"Bound to non-nil in START-SERVER.")
(defvar *backtrace-pprint-dispatch-table*
(let ((table (copy-pprint-dispatch nil)))
(flet ((print-string (stream string)
(cond (*print-escape*
(escape-string string stream
:map '((#\" . "\\\"")
(#\\ . "\\\\")
(#\newline . "\\n")
(#\return . "\\r"))))
(t (write-string string stream)))))
(set-pprint-dispatch 'string #'print-string 0 table)
table)))
(defvar *backtrace-printer-bindings*
`((*print-pretty* . t)
(*print-readably* . nil)
(*print-level* . 4)
(*print-length* . 6)
(*print-lines* . 1)
(*print-right-margin* . 200)
(*print-pprint-dispatch* . ,*backtrace-pprint-dispatch-table*))
"Pretter settings for printing backtraces.")
(defvar *default-worker-thread-bindings* '()
"An alist to initialize dynamic variables in worker threads.
The list has the form ((VAR . VALUE) ...). Each variable VAR will be
bound to the corresponding VALUE.")
(defun call-with-bindings (alist fun)
"Call FUN with variables bound according to ALIST.
ALIST is a list of the form ((VAR . VAL) ...)."
(if (null alist)
(funcall fun)
(let* ((rlist (reverse alist))
(vars (mapcar #'car rlist))
(vals (mapcar #'cdr rlist)))
(progv vars vals
(funcall fun)))))
(defmacro with-bindings (alist &body body)
"See `call-with-bindings'.
Bindings appearing earlier in the list take priority"
`(call-with-bindings ,alist (lambda () ,@body)))
(defvar *slyfuns* (make-hash-table)
"A map of Sly functions.")
(defmacro defslyfun (name arglist &body rest)
"A DEFUN for functions that Emacs can call by RPC."
`(progn
(defun ,name ,arglist ,@rest)
(setf (gethash ',name *slyfuns*) #',name)
(eval-when (:compile-toplevel :load-toplevel :execute)
(export ',name (symbol-package ',name)))))
(defun missing-arg ()
"A function that the compiler knows will never to return a value.
You can use (MISSING-ARG) as the initform for defstruct slots that
must always be supplied. This way the :TYPE slot option need not
include some arbitrary initial value like NIL."
(error "A required &KEY or &OPTIONAL argument was not supplied."))
(defmacro add-hook (place function)
"Add FUNCTION to the list of values on PLACE."
`(pushnew ,function ,place))
(defun run-hook (functions &rest arguments)
"Call each of FUNCTIONS with ARGUMENTS."
(dolist (function functions)
(apply function arguments)))
(defvar *new-connection-hook* '()
"This hook is run each time a connection is established.
The connection structure is given as the argument.
Backend code should treat the connection structure as opaque.")
(defvar *connection-closed-hook* '()
"This hook is run when a connection is closed.
The connection as passed as an argument.
Backend code should treat the connection structure as opaque.")
(defvar *pre-reply-hook* '()
"Hook run (without arguments) immediately before replying to an RPC.")
(defvar *after-init-hook* '()
"Hook run after user init files are loaded.")
(defstruct (connection
(:constructor %make-connection)
(:conc-name connection-)
(:print-function print-connection))
(socket (missing-arg) :type t :read-only t)
(socket-io (missing-arg) :type stream :read-only t)
(channel-counter 0 :type number)
(channels '() :type list)
(listeners '() :type list)
(inspectors '() :type list)
(indentation-cache (make-hash-table :test 'eq) :type hash-table)
(indentation-cache-packages '())
(communication-style nil :type (member nil :spawn :sigio :fd-handler))
)
(defun print-connection (conn stream depth)
(declare (ignore depth))
(print-unreadable-object (conn stream :type t :identity t)))
(defstruct (singlethreaded-connection (:include connection)
(:conc-name sconn.))
saved-sigint-handler
(event-queue '() :type list)
(events-enqueued 0 :type fixnum))
(defstruct (multithreaded-connection (:include connection)
(:conc-name mconn.))
reader-thread
control-thread
auto-flush-thread
indentation-cache-thread
(active-threads '() :type list)
)
(defvar *emacs-connection* nil
"The connection to Emacs currently in use.")
(defun make-connection (socket stream style)
(let ((conn (funcall (ecase style
(:spawn
#'make-multithreaded-connection)
((:sigio nil :fd-handler)
#'make-singlethreaded-connection))
:socket socket
:socket-io stream
:communication-style style)))
(run-hook *new-connection-hook* conn)
(send-to-sentinel `(:add-connection ,conn))
conn))
(defslyfun ping (tag)
tag)
(defun safe-backtrace ()
(ignore-errors
(call-with-debugging-environment
(lambda () (backtrace 0 nil)))))
(define-condition slynk-error (error)
((backtrace :initarg :backtrace :reader slynk-error.backtrace)
(condition :initarg :condition :reader slynk-error.condition))
(:report (lambda (c s) (princ (slynk-error.condition c) s)))
(:documentation "Condition which carries a backtrace."))
(defun signal-slynk-error (condition &optional (backtrace (safe-backtrace)))
(error 'slynk-error :condition condition :backtrace backtrace))
(defvar *debug-on-slynk-protocol-error* nil
"When non-nil invoke the system debugger on errors that were
signalled during decoding/encoding the wire protocol. Do not set this
to T unless you want to debug slynk internals.")
(defmacro with-slynk-error-handler ((connection) &body body)
"Close the connection on internal `slynk-error's."
(let ((conn (gensym)))
`(let ((,conn ,connection))
(handler-case
(handler-bind ((slynk-error
(lambda (condition)
(when *debug-on-slynk-protocol-error*
(invoke-default-debugger condition)))))
(progn . ,body))
(slynk-error (condition)
(close-connection ,conn
(slynk-error.condition condition)
(slynk-error.backtrace condition)))))))
(defmacro with-panic-handler ((connection) &body body)
"Close the connection on unhandled `serious-condition's."
(let ((conn (gensym)))
`(let ((,conn ,connection))
(handler-bind ((serious-condition
(lambda (condition)
(close-connection ,conn condition (safe-backtrace))
(abort condition))))
. ,body))))
(add-hook *new-connection-hook* 'notify-backend-of-connection)
(defun notify-backend-of-connection (connection)
(declare (ignore connection))
(emacs-connected))
(defmacro defvar-unbound (name &optional (doc-string ""))
"Convenience macro to declare unbound special variables with a
documentation string."
`(progn
(defvar ,name)
(setf (documentation ',name 'variable) ,doc-string)
',name))
(defvar *slynk-io-package*
(let ((package (make-package :slynk-io-package :use '())))
(import '(nil t quote) package)
package))
(defvar *log-events* nil)
(defvar *log-output* nil)
(defun init-log-output ()
(unless *log-output*
(setq *log-output* (real-output-stream *error-output*))))
(add-hook *after-init-hook* 'init-log-output)
(defun real-input-stream (stream)
(typecase stream
(synonym-stream
(real-input-stream (symbol-value (synonym-stream-symbol stream))))
(two-way-stream
(real-input-stream (two-way-stream-input-stream stream)))
(t stream)))
(defun real-output-stream (stream)
(typecase stream
(synonym-stream
(real-output-stream (symbol-value (synonym-stream-symbol stream))))
(two-way-stream
(real-output-stream (two-way-stream-output-stream stream)))
(t stream)))
(defvar *event-history* (make-array 40 :initial-element nil)
"A ring buffer to record events for better error messages.")
(defvar *event-history-index* 0)
(defvar *enable-event-history* t)
(defun log-event (format-string &rest args)
"Write a message to *terminal-io* when *log-events* is non-nil.
Useful for low level debugging."
(with-standard-io-syntax
(let ((*print-readably* nil)
(*print-pretty* nil)
(*package* *slynk-io-package*))
(when *enable-event-history*
(setf (aref *event-history* *event-history-index*)
(format nil "~?" format-string args))
(setf *event-history-index*
(mod (1+ *event-history-index*) (length *event-history*))))
(when *log-events*
(write-string (escape-non-ascii (format nil "~?" format-string args))
*log-output*)
(force-output *log-output*)))))
(defun event-history-to-list ()
"Return the list of events (older events first)."
(let ((arr *event-history*)
(idx *event-history-index*))
(concatenate 'list (subseq arr idx) (subseq arr 0 idx))))
(defun clear-event-history ()
(fill *event-history* nil)
(setq *event-history-index* 0))
(defun dump-event-history (stream)
(dolist (e (event-history-to-list))
(dump-event e stream)))
(defun dump-event (event stream)
(cond ((stringp event)
(write-string (escape-non-ascii event) stream))
((null event))
(t
(write-string
(escape-non-ascii (format nil "Unexpected event: ~A~%" event))
stream))))
(defun escape-non-ascii (string)
"Return a string like STRING but with non-ascii chars escaped."
(cond ((ascii-string-p string) string)
(t (with-output-to-string (out)
(loop for c across string do
(cond ((ascii-char-p c) (write-char c out))
(t (format out "\\x~4,'0X" (char-code c)))))))))
(defun ascii-string-p (o)
(and (stringp o)
(every #'ascii-char-p o)))
(defun ascii-char-p (c)
(<= (char-code c) 127))
(defmacro destructure-case (value &body patterns)
"Dispatch VALUE to one of PATTERNS.
A cross between `case' and `destructuring-bind'.
The pattern syntax is:
((HEAD . ARGS) . BODY)
The list of patterns is searched for a HEAD `eq' to the car of
VALUE. If one is found, the BODY is executed with ARGS bound to the
corresponding values in the CDR of VALUE."
(let ((operator (gensym "op-"))
(operands (gensym "rand-"))
(tmp (gensym "tmp-")))
`(let* ((,tmp ,value)
(,operator (car ,tmp))
(,operands (cdr ,tmp)))
(case ,operator
,@(loop for (pattern . body) in patterns collect
(if (eq pattern t)
`(t ,@body)
(destructuring-bind (op &rest rands) pattern
`(,op (destructuring-bind ,rands ,operands
,@body)))))
,@(if (eq (caar (last patterns)) t)
'()
`((t (error "destructure-case failed: ~S" ,tmp))))))))
(defmacro channels () `(connection-channels *emacs-connection*))
(defmacro channel-counter () `(connection-channel-counter *emacs-connection*))
(defclass channel ()
((id :initform (incf (channel-counter))
:reader channel-id)
(thread :initarg :thread :initform (current-thread)
:reader channel-thread)
(name :initarg :name :initform nil)))
(defmethod initialize-instance :after ((ch channel) &key)
(with-slots (thread) ch
(when (use-threads-p)
(setf thread (spawn-channel-thread *emacs-connection* ch)))
(slynk-backend:send thread `(:serve-channel ,ch)))
(setf (channels) (nconc (channels) (list ch))))
(defmethod print-object ((c channel) stream)
(print-unreadable-object (c stream :type t)
(with-slots (id name) c
(format stream "~d ~a" id name))))
(defmethod drop-unprocessed-events (channel)
(declare (ignore channel)))
(defun find-channel (id)
(find id (channels) :key #'channel-id))
(defun find-channel-thread (channel)
(channel-thread channel))
(defun channel-thread-id (channel)
(slynk-backend:thread-id (channel-thread channel)))
(defmethod close-channel (channel &key)
(let ((probe (find-channel (channel-id channel))))
(cond (probe (setf (channels) (delete probe (channels))))
(t (error "Can't close invalid channel: ~a" channel)))))
(defgeneric channel-send (channel selector args)
(:documentation "Send to CHANNEL the message SELECTOR with ARGS."))
(defmacro define-channel-method (selector (channel &rest args) &body body)
`(defmethod channel-send (,channel (selector (eql ',selector)) args)
(destructuring-bind ,args args
. ,body)))
(define-channel-method :teardown ((c channel))
(if (use-threads-p)
(throw 'stop-processing 'listener-teardown)
(close-channel c)))
(defun send-to-remote-channel (channel-id msg)
(send-to-emacs `(:channel-send ,channel-id ,msg)))
(defclass listener ()
((out :initarg :out :type stream :reader listener-out)
(in :initarg :in :type stream :reader listener-in)
(env)))
(defmacro listeners () `(connection-listeners *emacs-connection*))
(defmethod initialize-instance :after ((l listener) &key initial-env)
(with-slots (out in env) l
(let ((io (make-two-way-stream in out)))
(setf env
(append
initial-env
`((cl:*standard-output* . ,out)
(cl:*standard-input* . ,in)
(cl:*trace-output* . ,out)
(cl:*error-output* . ,out)
(cl:*debug-io* . ,io)
(cl:*query-io* . ,io)
(cl:*terminal-io* . ,io)))))
(assert out nil "Must have an OUT stream")
(assert in nil "Must have an IN stream")
(assert env nil "Must have an ENV"))
(setf (listeners) (nconc (listeners)
(list l))))
(defun call-with-listener (listener fn &optional saving)
(with-slots (env) listener
(with-bindings env
(unwind-protect (funcall fn)
(when saving
(loop for binding in env
do (setf (cdr binding) (symbol-value (car binding)))))))))
(defmacro with-listener-bindings (listener &body body)
"Execute BODY inside LISTENER's environment"
`(call-with-listener ,listener (lambda () ,@body)))
(defmacro saving-listener-bindings (listener &body body)
"Execute BODY inside LISTENER's environment, update it afterwards."
`(call-with-listener ,listener (lambda () ,@body) 'saving))
(defmacro with-default-listener ((connection) &body body)
"Execute BODY with in CONNECTION's default listener."
(let ((listener-sym (gensym))
(body-fn-sym (gensym)))
`(let ((,listener-sym (default-listener ,connection))
(,body-fn-sym #'(lambda () ,@body)))
(if ,listener-sym
(with-listener-bindings ,listener-sym
(funcall ,body-fn-sym))
(funcall ,body-fn-sym)))))
(defun default-listener (connection)
(first (connection-listeners connection)))
(defun flush-listener-streams (listener)
(with-slots (in out) listener
(force-output out)
#-armedbear
(slynk-gray::reset-stream-line-column out)
(clear-input in)))
(defmethod close-listener (l)
(with-slots (in out) l (close in) (close out))
(setf (listeners) (delete l (listeners))))
(defvar *sly-interrupts-enabled*)
(defmacro with-interrupts-enabled% (flag body)
`(progn
,@(if flag '((check-sly-interrupts)))
(multiple-value-prog1
(let ((*sly-interrupts-enabled* ,flag))
,@body)
,@(if flag '((check-sly-interrupts))))))
(defmacro with-sly-interrupts (&body body)
`(with-interrupts-enabled% t ,body))
(defmacro without-sly-interrupts (&body body)
`(with-interrupts-enabled% nil ,body))
(defun queue-thread-interrupt (thread function)
(interrupt-thread thread
(lambda ()
(when (invoke-or-queue-interrupt function)
(wake-thread thread)))))
(defun invoke-or-queue-interrupt (function)
(log-event "invoke-or-queue-interrupt: ~a~%" function)
(cond ((not (boundp '*sly-interrupts-enabled*))
(without-sly-interrupts
(funcall function)))
(*sly-interrupts-enabled*
(log-event "interrupts-enabled~%")
(funcall function))
(t
(setq *pending-sly-interrupts*
(nconc *pending-sly-interrupts*
(list function)))
(cond ((cdr *pending-sly-interrupts*)
(log-event "too many queued interrupts~%")
(with-simple-restart (continue "Continue from interrupt")
(handler-bind ((serious-condition #'invoke-sly-debugger))
(check-sly-interrupts))))
(t
(log-event "queue-interrupt: ~a~%" function)
(when *interrupt-queued-handler*
(funcall *interrupt-queued-handler*))
t)))))
(defvar *send-counter*)
(defmacro with-connection ((connection) &body body)
"Execute BODY in the context of CONNECTION."
`(let ((connection ,connection)
(function (lambda () . ,body)))
(if (eq *emacs-connection* connection)
(funcall function)
(let ((*emacs-connection* connection)
(*pending-sly-interrupts* '())
(*send-counter* 0))
(without-sly-interrupts
(with-slynk-error-handler (connection)
(with-default-listener (connection)
(call-with-debugger-hook #'slynk-debugger-hook
function))))))))
(defun call-with-retry-restart (msg thunk)
(loop (with-simple-restart (retry "~a" msg)
(return (funcall thunk)))))
(defmacro with-retry-restart ((&key (msg "Retry.")) &body body)
(check-type msg string)
`(call-with-retry-restart ,msg (lambda () ,@body)))
(defvar *connections* '()
"List of all active connections, with the most recent at the front.")
(defvar *servers* '()
"A list ((server-socket port thread) ...) describing the listening sockets.
Used to close sockets on server shutdown or restart.")
(defun default-connection ()
"Return the 'default' Emacs connection.
This connection can be used to talk with Emacs when no specific
connection is in use, i.e. *EMACS-CONNECTION* is NIL.
The default connection is defined (quite arbitrarily) as the most
recently established one."
(car *connections*))
(defun start-sentinel ()
(unless (find-registered 'sentinel)
(let ((thread (spawn #'sentinel :name "Slynk Sentinel")))
(register-thread 'sentinel thread))))
(defun sentinel ()
(catch 'exit-sentinel
(loop (sentinel-serve (receive)))))
(defun send-to-sentinel (msg)
(let ((sentinel (find-registered 'sentinel)))
(cond (sentinel (send sentinel msg))
(t (sentinel-serve msg)))))
(defun sentinel-serve (msg)
(destructure-case msg
((:add-connection conn)
(push conn *connections*))
((:close-connection connection condition backtrace)
(close-connection% connection condition backtrace)
(sentinel-maybe-exit))
((:add-server socket port thread)
(push (list socket port thread) *servers*))
((:stop-server key port)
(sentinel-stop-server key port)
(sentinel-maybe-exit))))
(defun sentinel-stop-server (key value)
(let ((probe (find value *servers* :key (ecase key
(:socket #'car)
(:port #'cadr)))))
(cond (probe
(setq *servers* (delete probe *servers*))
(destructuring-bind (socket _port thread) probe
(declare (ignore _port))
(ignore-errors (close-socket socket))
(when (and thread
(thread-alive-p thread)
(not (eq thread (current-thread))))
(ignore-errors (kill-thread thread)))))
(t
(warn "No server for ~s: ~s" key value)))))
(defun sentinel-maybe-exit ()
(when (and (null *connections*)
(null *servers*)
(and (current-thread)
(eq (find-registered 'sentinel)
(current-thread))))
(register-thread 'sentinel nil)
(throw 'exit-sentinel nil)))
(defun use-threads-p ()
(eq (connection-communication-style *emacs-connection*) :spawn))
(defun current-thread-id ()
(thread-id (current-thread)))
(declaim (inline ensure-list))
(defun ensure-list (thing)
(if (listp thing) thing (list thing)))
(defun symbol-status (symbol &optional (package (symbol-package symbol)))
"Returns one of
:INTERNAL if the symbol is _present_ in PACKAGE as an _internal_ symbol,
:EXTERNAL if the symbol is _present_ in PACKAGE as an _external_ symbol,
:INHERITED if the symbol is _inherited_ by PACKAGE through USE-PACKAGE,
but is not _present_ in PACKAGE,
or NIL if SYMBOL is not _accessible_ in PACKAGE.
Be aware not to get confused with :INTERNAL and how \"internal
symbols\" are defined in the spec; there is a slight mismatch of
definition with the Spec and what's commonly meant when talking
about internal symbols most times. As the spec says:
In a package P, a symbol S is
_accessible_ if S is either _present_ in P itself or was
inherited from another package Q (which implies
that S is _external_ in Q.)
You can check that with: (AND (SYMBOL-STATUS S P) T)
_present_ if either P is the /home package/ of S or S has been
imported into P or exported from P by IMPORT, or
EXPORT respectively.
Or more simply, if S is not _inherited_.
You can check that with: (LET ((STATUS (SYMBOL-STATUS S P)))
(AND STATUS
(NOT (EQ STATUS :INHERITED))))
_external_ if S is going to be inherited into any package that
/uses/ P by means of USE-PACKAGE, MAKE-PACKAGE, or
DEFPACKAGE.
Note that _external_ implies _present_, since to
make a symbol _external_, you'd have to use EXPORT
which will automatically make the symbol _present_.
You can check that with: (EQ (SYMBOL-STATUS S P) :EXTERNAL)
_internal_ if S is _accessible_ but not _external_.
You can check that with: (LET ((STATUS (SYMBOL-STATUS S P)))
(AND STATUS
(NOT (EQ STATUS :EXTERNAL))))
Notice that this is *different* to
(EQ (SYMBOL-STATUS S P) :INTERNAL)
because what the spec considers _internal_ is split up into two
explicit pieces: :INTERNAL, and :INHERITED; just as, for instance,
CL:FIND-SYMBOL does.
The rationale is that most times when you speak about \"internal\"
symbols, you're actually not including the symbols inherited
from other packages, but only about the symbols directly specific
to the package in question.
"
(when package (check-type symbol symbol) (check-type package package)
(multiple-value-bind (present-symbol status)
(find-symbol (symbol-name symbol) package)
(and (eq symbol present-symbol) status))))
(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."
(eq (symbol-status symbol package) :external))
(defun classify-symbol (symbol)
"Returns a list of classifiers that classify SYMBOL according to its
underneath objects (e.g. :BOUNDP if SYMBOL constitutes a special
variable.) The list may contain the following classification
keywords: :BOUNDP, :FBOUNDP, :CONSTANT, :GENERIC-FUNCTION,
:TYPESPEC, :CLASS, :MACRO, :SPECIAL-OPERATOR, and/or :PACKAGE"
(check-type symbol symbol)
(flet ((type-specifier-p (s)
(or (documentation s 'type)
(not (eq (type-specifier-arglist s) :not-available)))))
(let (result)
(when (boundp symbol) (push (if (constantp symbol)
:constant :boundp) result))
(when (fboundp symbol) (push :fboundp result))
(when (type-specifier-p symbol) (push :typespec result))
(when (find-class symbol nil) (push :class result))
(when (macro-function symbol) (push :macro result))
(when (special-operator-p symbol) (push :special-operator result))
(when (find-package symbol) (push :package result))
(when (and (fboundp symbol)
(typep (ignore-errors (fdefinition symbol))
'generic-function))
(push :generic-function result))
result)))
(defvar *communication-style* (preferred-communication-style))
(defvar *dont-close* nil
"Default value of :dont-close argument to start-server and
create-server.")
(defparameter *loopback-interface* "localhost")
(defun start-server (port-file
&key (style *communication-style*)
(dont-close *dont-close*))
"Start the server and write the listen port number to PORT-FILE.
This is the entry point for Emacs."
(setq *m-x-sly-from-emacs* t)
(setup-server 0
(lambda (port) (announce-server-port port-file port))
style dont-close nil))
(defun create-server (&key (port default-server-port)
(style *communication-style*)
(dont-close *dont-close*)
interface
backlog)
"Start a SLYNK server on PORT running in STYLE.
If DONT-CLOSE is true then the listen socket will accept multiple
connections, otherwise it will be closed after the first.
Optionally, an INTERFACE could be specified and swank will bind
the PORT on this interface. By default, interface is \"localhost\"."
(let ((*loopback-interface* (or interface
*loopback-interface*)))
(setup-server port #'simple-announce-function
style dont-close backlog)))
(defun find-external-format-or-lose (coding-system)
(or (find-external-format coding-system)
(error "Unsupported coding system: ~s" coding-system)))
(defmacro restart-loop (form &body clauses)
"Executes FORM, with restart-case CLAUSES which have a chance to modify FORM's
environment before trying again (by returning normally) or giving up (through an
explicit transfer of control), all within an implicit block named nil.
e.g.: (restart-loop (http-request url) (use-value (new) (setq url new)))"
`(loop (restart-case (return ,form) ,@clauses)))
(defun socket-quest (port backlog)
"Attempt o create a socket on PORT.
Add a restart, prompting user to enter a new port if PORT is already
taken."
(restart-loop (create-socket *loopback-interface* port :backlog backlog)
(use-value (&optional (new-port (1+ port)))
:report (lambda (stream) (format stream "Try a port other than ~D" port))
:interactive
(lambda ()
(format *query-io* "Enter port (defaults to ~D): " (1+ port))
(finish-output *query-io*) (ignore-errors (list (parse-integer (read-line *query-io*)))))
(setq port new-port))))
(defun setup-server (port announce-fn style dont-close backlog)
(init-log-output)
(let* ((socket (socket-quest port backlog))
(port (local-port socket)))
(funcall announce-fn port)
(labels ((serve () (accept-connections socket style dont-close))
(note () (send-to-sentinel `(:add-server ,socket ,port
,(current-thread))))
(serve-loop () (note) (loop do (serve) while dont-close)))
(ecase style
(:spawn (initialize-multiprocessing
(lambda ()
(start-sentinel)
(spawn #'serve-loop :name (format nil "Slynk ~s" port)))))
((:fd-handler :sigio)
(note)
(add-fd-handler socket #'serve))
((nil) (serve-loop))))
port))
(defun stop-server (port)
"Stop server running on PORT."
(send-to-sentinel `(:stop-server :port ,port)))
(defun restart-server (&key (port default-server-port)
(style *communication-style*)
(dont-close *dont-close*))
"Stop the server listening on PORT, then start a new SLYNK server
on PORT running in STYLE. If DONT-CLOSE is true then the listen socket
will accept multiple connections, otherwise it will be closed after the
first."
(stop-server port)
(sleep 5)
(create-server :port port :style style :dont-close dont-close))
(defun accept-connections (socket style dont-close)
(unwind-protect
(let ((client (accept-connection socket :external-format nil
:buffering t)))
(authenticate-client client)
(serve-requests (make-connection socket client style)))
(unless dont-close
(send-to-sentinel `(:stop-server :socket ,socket)))))
(defun authenticate-client (stream)
(let ((secret (sly-secret)))
(when secret
(set-stream-timeout stream 20)
(let ((first-val (read-packet stream)))
(unless (and (stringp first-val) (string= first-val secret))
(error "Incoming connection doesn't know the password.")))
(set-stream-timeout stream nil))))
(defun sly-secret ()
"Finds the magic secret from the user's home directory. Returns nil
if the file doesn't exist; otherwise the first line of the file."
(with-open-file (in
(merge-pathnames (user-homedir-pathname) #p".sly-secret")
:if-does-not-exist nil)
(and in (read-line in nil ""))))
(defun serve-requests (connection)
"Read and process all requests on connections."
(etypecase connection
(multithreaded-connection
(spawn-threads-for-connection connection))
(singlethreaded-connection
(ecase (connection-communication-style connection)
((nil) (simple-serve-requests connection))
(:sigio (install-sigio-handler connection))
(:fd-handler (install-fd-handler connection))))))
(defun stop-serving-requests (connection)
(etypecase connection
(multithreaded-connection
(cleanup-connection-threads connection))
(singlethreaded-connection
(ecase (connection-communication-style connection)
((nil))
(:sigio (deinstall-sigio-handler connection))
(:fd-handler (deinstall-fd-handler connection))))))
(defun announce-server-port (file port)
(with-open-file (s file
:direction :output
:if-exists :error
:if-does-not-exist :create)
(format s "~S~%" port))
(simple-announce-function port))
(defun simple-announce-function (port)
(when *slynk-debug-p*
(format *log-output* "~&;; Slynk started at port: ~D.~%" port)
(force-output *log-output*)))
(defun decode-message (stream)
"Read an S-expression from STREAM using the SLY protocol."
(log-event "decode-message~%")
(without-sly-interrupts
(handler-bind ((error #'signal-slynk-error))
(handler-case (read-message stream *slynk-io-package*)
(slynk-reader-error (c)
`(:reader-error ,(slynk-reader-error.packet c)
,(slynk-reader-error.cause c)))))))
(defun encode-message (message stream)
"Write an S-expression to STREAM using the SLY protocol."
(log-event "encode-message~%")
(without-sly-interrupts
(handler-bind ((error #'signal-slynk-error))
(write-message message *slynk-io-package* stream))))
(defvar *sly-db-quit-restart* nil
"The restart that will be invoked when the user calls sly-db-quit.")
(defmacro with-top-level-restart ((connection k) &body body)
`(with-connection (,connection)
(restart-case
(let ((*sly-db-quit-restart* (find-restart 'abort)))
,@body)
(abort (&optional v)
:report "Return to SLY's top level."
(declare (ignore v))
(force-user-output)
,k))))
(defun handle-requests (connection &optional timeout)
"Read and process :emacs-rex requests.
The processing is done in the extent of the toplevel restart."
(with-connection (connection)
(cond (*sly-db-quit-restart*
(process-requests timeout))
(t
(tagbody
start
(with-top-level-restart (connection (go start))
(process-requests timeout)))))))
(defvar-unbound *channel*
"Current CHANNEL instance used by :EMACS-CHANNEL-SEND messages.")
(defun process-requests (timeout)
"Read and process requests from Emacs.
TIMEOUT has the same meaning as in WAIT-FOR-EVENT."
(catch 'stop-processing
(loop
(multiple-value-bind (event timed-out-p)
(wait-for-event `(or (:emacs-rex . _)
(:emacs-channel-send . _))
timeout)
(when timed-out-p (return))
(destructure-case event
((:emacs-rex &rest args) (apply #'eval-for-emacs args))
((:emacs-channel-send *channel* (selector &rest args))
(channel-send *channel* selector args)))))))
(defun spawn-channel-thread (connection channel)
"Spawn a listener thread for CONNECTION and CHANNEL.
The new thread will block waiting for a :SERVE-CHANNEL message, then
process all requests in series until the :TEARDOWN message, at which
point the thread terminates and CHANNEL is closed."
(slynk-backend:spawn
(lambda ()
(with-connection (connection)
(unwind-protect
(destructure-case
(slynk-backend:receive)
((:serve-channel c)
(assert (eq c channel))
(loop
(with-top-level-restart (connection
(drop-unprocessed-events channel))
(when (eq (process-requests nil)
'listener-teardown)
(return))))))
(close-channel channel))))
:name (with-slots (id name) channel
(format nil "sly-channel-~a-~a" id name))))
(defun current-socket-io ()
(connection-socket-io *emacs-connection*))
(defun close-connection (connection condition backtrace)
(send-to-sentinel `(:close-connection ,connection ,condition ,backtrace)))
(defun close-connection% (c condition backtrace)
(let ((*debugger-hook* nil))
(log-event "close-connection: ~a ...~%" condition)
(format *log-output* "~&;; slynk:close-connection: ~A~%"
(escape-non-ascii (safe-condition-message condition)))
(let ((*emacs-connection* c))
(format *log-output* "~&;; closing ~a channels~%" (length (connection-channels c)))
(mapc #'(lambda (c) (close-channel c :force t)) (connection-channels c))
(format *log-output* "~&;; closing ~a listeners~%" (length (connection-listeners c)))
(ignore-errors
(mapc #'close-listener (connection-listeners c))))
(stop-serving-requests c)
(close (connection-socket-io c))
(setf *connections* (remove c *connections*))
(run-hook *connection-closed-hook* c)
(when (and condition (not (typep condition 'end-of-file)))
(finish-output *log-output*)
(format *log-output* "~&;; Event history start:~%")
(dump-event-history *log-output*)
(format *log-output* "~
;; Event history end.~%~
;; Backtrace:~%~{~A~%~}~
;; Connection to Emacs lost. [~%~
;; condition: ~A~%~
;; type: ~S~%~
;; style: ~S]~%"
(loop for (i f) in backtrace
collect
(ignore-errors
(format nil "~d: ~a" i (escape-non-ascii f))))
(escape-non-ascii (safe-condition-message condition) )
(type-of condition)
(connection-communication-style c)))
(finish-output *log-output*)
(log-event "close-connection ~a ... done.~%" condition)))
(defun read-loop (connection)
(let ((input-stream (connection-socket-io connection))
(control-thread (mconn.control-thread connection)))
(with-slynk-error-handler (connection)
(loop (send control-thread (decode-message input-stream))))))
(defun dispatch-loop (connection)
(let ((*emacs-connection* connection))
(with-panic-handler (connection)
(loop (dispatch-event connection (receive))))))
(defgeneric thread-for-evaluation (connection id)
(:documentation "Find or create a thread to evaluate the next request.")
(:method ((connection multithreaded-connection) (id (eql t)))
(spawn-worker-thread connection))
(:method ((connection multithreaded-connection) (id (eql :find-existing)))
(car (mconn.active-threads connection)))
(:method (connection (id integer))
(declare (ignorable connection))
(find-thread id))
(:method ((connection singlethreaded-connection) id)
(declare (ignorable connection connection id))
(current-thread)))
(defun interrupt-worker-thread (connection id)
(let ((thread (thread-for-evaluation connection
(cond ((eq id t) :find-existing)
(t id)))))
(log-event "interrupt-worker-thread: ~a ~a~%" id thread)
(if thread
(etypecase connection
(multithreaded-connection
(queue-thread-interrupt thread #'simple-break))
(singlethreaded-connection
(simple-break)))
(encode-message (list :debug-condition (current-thread-id)
(format nil "Thread with id ~a not found"
id))
(current-socket-io)))))
(defun spawn-worker-thread (connection)
(spawn (lambda ()
(with-bindings *default-worker-thread-bindings*
(with-top-level-restart (connection nil)
(let ((thread (current-thread)))
(unwind-protect
(apply #'eval-for-emacs
(cdr (wait-for-event `(:emacs-rex . _))))
(remove-active-thread connection thread))))))
:name "slynk-worker"))
(defun add-active-thread (connection thread)
(etypecase connection
(multithreaded-connection
(push thread (mconn.active-threads connection)))
(singlethreaded-connection)))
(defun remove-active-thread (connection thread)
(etypecase connection
(multithreaded-connection
(setf (mconn.active-threads connection)
(delete thread (mconn.active-threads connection) :count 1)))
(singlethreaded-connection)))
(defun dispatch-event (connection event)
"Handle an event triggered either by Emacs or within Lisp."
(log-event "dispatch-event: ~s~%" event)
(destructure-case event
((:emacs-rex form package thread-id id &rest extra-rex-options)
(let ((thread (thread-for-evaluation connection thread-id)))
(cond (thread
(add-active-thread connection thread)
(send-event thread `(:emacs-rex ,form ,package ,id ,@extra-rex-options)))
(t
(encode-message
(list :invalid-rpc id
(format nil "Thread not found: ~s" thread-id))
(current-socket-io))))))
((:return thread &rest args)
(declare (ignore thread))
(encode-message `(:return ,@args) (current-socket-io)))
((:emacs-interrupt thread-id)
(interrupt-worker-thread connection thread-id))
(((:write-string
:debug :debug-condition :debug-activate :debug-return :channel-send
:presentation-start :presentation-end
:new-package :new-features :ed :indentation-update
:eval :eval-no-wait :background-message :inspect :ping
:y-or-n-p :read-from-minibuffer :read-string :read-aborted :test-delay)
&rest _)
(declare (ignore _))
(encode-message event (current-socket-io)))
(((:emacs-pong :emacs-return :emacs-return-string) thread-id &rest args)
(send-event (find-thread thread-id) (cons (car event) args)))
((:emacs-channel-send channel-id msg)
(let* ((ch (find-channel channel-id))
(thread (and ch (find-channel-thread ch))))
(cond ((and ch thread)
(send-event thread `(:emacs-channel-send ,ch ,msg)))
(ch
(encode-message
(list :invalid-channel channel-id
"No suitable threads for channel")
(current-socket-io)))
(t
(encode-message
(list :invalid-channel channel-id "Channel not found")
(current-socket-io))))))
((:reader-error packet condition)
(encode-message `(:reader-error ,packet
,(safe-condition-message condition))
(current-socket-io)))))
(defun send-event (thread event)
(log-event "send-event: ~s ~s~%" thread event)
(let ((c *emacs-connection*))
(etypecase c
(multithreaded-connection
(send thread event))
(singlethreaded-connection
(setf (sconn.event-queue c) (nconc (sconn.event-queue c) (list event)))
(setf (sconn.events-enqueued c) (mod (1+ (sconn.events-enqueued c))
most-positive-fixnum))))))
(defun send-to-emacs (event)
"Send EVENT to Emacs."
(without-sly-interrupts
(let ((c *emacs-connection*))
(etypecase c
(multithreaded-connection
(send (mconn.control-thread c) event))
(singlethreaded-connection
(dispatch-event c event)))
(maybe-slow-down))))
(defun make-thread-bindings-aware-lambda (fn)
(let ((connection *emacs-connection*)
(send-counter *send-counter*))
(lambda (&rest args)
(let ((*emacs-connection* connection)
(*send-counter* send-counter))
(apply fn args)))))
(defconstant send-counter-limit 100)
(defun maybe-slow-down ()
(let ((counter (incf *send-counter*)))
(when (< send-counter-limit counter)
(setf *send-counter* 0)
(ping-pong))))
(defun ping-pong ()
(let* ((tag (make-tag))
(pattern `(:emacs-pong ,tag)))
(send-to-emacs `(:ping ,(current-thread-id) ,tag))
(wait-for-event pattern)))
(defun wait-for-event (pattern &optional timeout)
"Scan the event queue for PATTERN and return the event.
If TIMEOUT is NIL wait until a matching event is enqued.
If TIMEOUT is T only scan the queue without waiting.
The second return value is t if the timeout expired before a matching
event was found."
(log-event "wait-for-event: ~s ~s~%" pattern timeout)
(without-sly-interrupts
(let ((c *emacs-connection*))
(etypecase c
(multithreaded-connection
(receive-if (lambda (e) (event-match-p e pattern)) timeout))
(singlethreaded-connection
(wait-for-event/event-loop c pattern timeout))))))
(defun wait-for-event/event-loop (connection pattern timeout)
(assert (or (not timeout) (eq timeout t)))
(loop
(check-sly-interrupts)
(let ((event (poll-for-event connection pattern)))
(when event (return (car event))))
(let ((events-enqueued (sconn.events-enqueued connection))
(ready (wait-for-input (list (current-socket-io)) timeout)))
(cond ((and timeout (not ready))
(return (values nil t)))
((or (/= events-enqueued (sconn.events-enqueued connection))
(eq ready :interrupt))
)
(t
(assert (equal ready (list (current-socket-io))))
(dispatch-event connection
(decode-message (current-socket-io))))))))
(defun poll-for-event (connection pattern)
(let* ((c connection)
(tail (member-if (lambda (e) (event-match-p e pattern))
(sconn.event-queue c))))
(when tail
(setf (sconn.event-queue c)
(nconc (ldiff (sconn.event-queue c) tail) (cdr tail)))
tail)))
(defun event-match-p (event pattern)
(cond ((or (keywordp pattern) (numberp pattern) (stringp pattern)
(member pattern '(nil t)))
(equal event pattern))
((symbolp pattern) t)
((consp pattern)
(case (car pattern)
((or) (some (lambda (p) (event-match-p event p)) (cdr pattern)))
(t (and (consp event)
(and (event-match-p (car event) (car pattern))
(event-match-p (cdr event) (cdr pattern)))))))
(t (error "Invalid pattern: ~S" pattern))))
(defun spawn-threads-for-connection (connection)
(setf
(mconn.control-thread connection)
(spawn
(lambda ()
"Spawns a reader and indentation threads, then calls DISPATCH-LOOP."
(setf (mconn.reader-thread connection) (spawn (lambda () (read-loop connection))
:name "reader-thread"))
(setf (mconn.indentation-cache-thread connection)
(spawn (lambda () (indentation-cache-loop connection))
:name "slynk-indentation-cache-thread"))
(dispatch-loop connection))
:name "control-thread"))
connection)
(defun cleanup-connection-threads (connection)
(let* ((c connection)
(threads (list (mconn.reader-thread c)
(mconn.control-thread c)
(mconn.auto-flush-thread c)
(mconn.indentation-cache-thread c))))
(dolist (thread threads)
(when (and thread
(thread-alive-p thread)
(not (equal (current-thread) thread)))
(ignore-errors (kill-thread thread))))))
(defun install-sigio-handler (connection)
(add-sigio-handler (connection-socket-io connection)
(lambda () (process-io-interrupt connection)))
(handle-requests connection t))
(defvar *io-interupt-level* 0)
(defun process-io-interrupt (connection)
(log-event "process-io-interrupt ~d ...~%" *io-interupt-level*)
(let ((*io-interupt-level* (1+ *io-interupt-level*)))
(invoke-or-queue-interrupt
(lambda () (handle-requests connection t))))
(log-event "process-io-interrupt ~d ... done ~%" *io-interupt-level*))
(defun deinstall-sigio-handler (connection)
(log-event "deinstall-sigio-handler...~%")
(remove-sigio-handlers (connection-socket-io connection))
(log-event "deinstall-sigio-handler...done~%"))
(defun install-fd-handler (connection)
(add-fd-handler (connection-socket-io connection)
(lambda () (handle-requests connection t)))
(setf (sconn.saved-sigint-handler connection)
(install-sigint-handler
(lambda ()
(invoke-or-queue-interrupt
(lambda () (dispatch-interrupt-event connection))))))
(handle-requests connection t))
(defun dispatch-interrupt-event (connection)
(with-connection (connection)
(dispatch-event connection `(:emacs-interrupt ,(current-thread-id)))))
(defun deinstall-fd-handler (connection)
(log-event "deinstall-fd-handler~%")
(remove-fd-handlers (connection-socket-io connection))
(install-sigint-handler (sconn.saved-sigint-handler connection)))
(defun simple-serve-requests (connection)
(unwind-protect
(with-connection (connection)
(call-with-user-break-handler
(lambda ()
(invoke-or-queue-interrupt
(lambda () (dispatch-interrupt-event connection))))
(lambda ()
(with-simple-restart (close-connection "Close SLY connection.")
(let* ((stdin (real-input-stream *standard-input*))
(*standard-input* (make-repl-input-stream connection
stdin)))
(tagbody toplevel
(with-top-level-restart (connection (go toplevel))
(simple-repl))))))))
(close-connection connection nil (safe-backtrace))))
(define-condition end-of-repl-input (end-of-file) ())
(defun simple-repl ()
(loop
(format t "~a> " (package-string-for-prompt *package*))
(force-output)
(let ((form (handler-case (read)
(end-of-repl-input () (return)))))
(let* ((- form)
(values (multiple-value-list (eval form))))
(setq *** ** ** * * (car values)
/// // // / / values
+++ ++ ++ + + form)
(cond ((null values) (format t "; No values~&"))
(t (mapc (lambda (v) (format t "~s~&" v)) values)))))))
(defun make-repl-input-stream (connection stdin)
(make-input-stream
(lambda () (repl-input-stream-read connection stdin))))
(defun repl-input-stream-read (connection stdin)
(loop
(let* ((socket (connection-socket-io connection))
(inputs (list socket stdin))
(ready (wait-for-input inputs)))
(cond ((eq ready :interrupt)
(check-sly-interrupts))
((member socket ready)
(with-simple-restart (process-input "Continue reading input.")
(let ((*sly-db-quit-restart* (find-restart 'process-input)))
(with-default-listener (connection)
(handle-requests connection t)))))
((member stdin ready)
(return (read-non-blocking stdin)))
(t (assert (null ready)))))))
(defun read-non-blocking (stream)
(with-output-to-string (str)
(handler-case
(loop (let ((c (read-char-no-hang stream)))
(unless c (return))
(write-char c str)))
(end-of-file () (error 'end-of-repl-input :stream stream)))))
(defvar *sly-features* nil
"The feature list that has been sent to Emacs.")
(defun send-oob-to-emacs (object)
(send-to-emacs object))
(defun force-user-output ()
(with-default-listener (*emacs-connection*)
(force-output *standard-output*)))
(add-hook *pre-reply-hook* 'force-user-output)
(defun clear-user-input ()
(with-default-listener (*emacs-connection*)
(clear-input *standard-input*)))
(defvar *tag-counter* 0)
(defun make-tag ()
(setq *tag-counter* (mod (1+ *tag-counter*) (expt 2 22))))
(defun y-or-n-p-in-emacs (format-string &rest arguments)
"Like y-or-n-p, but ask in the Emacs minibuffer."
(let ((tag (make-tag))
(question (apply #'format nil format-string arguments)))
(force-output)
(send-to-emacs `(:y-or-n-p ,(current-thread-id) ,tag ,question))
(third (wait-for-event `(:emacs-return ,tag result)))))
(defun read-from-minibuffer-in-emacs (prompt &optional initial-value)
"Ask user a question in Emacs' minibuffer. Returns \"\" when user
entered nothing, returns NIL when user pressed C-g."
(check-type prompt string) (check-type initial-value (or null string))
(let ((tag (make-tag)))
(force-output)
(send-to-emacs `(:read-from-minibuffer ,(current-thread-id) ,tag
,prompt ,initial-value))
(third (wait-for-event `(:emacs-return ,tag result)))))
(defun process-form-for-emacs (form)
"Returns a string which emacs will read as equivalent to
FORM. FORM can contain lists, strings, characters, symbols and
numbers.
Characters are converted emacs' ?<char> notaion, strings are left
as they are (except for espacing any nested \" chars, numbers are
printed in base 10 and symbols are printed as their symbol-name
converted to lower case."
(etypecase form
(string (format nil "~S" form))
(cons (format nil "(~A . ~A)"
(process-form-for-emacs (car form))
(process-form-for-emacs (cdr form))))
(character (format nil "?~C" form))
(symbol (concatenate 'string (when (eq (symbol-package form)
#.(find-package "KEYWORD"))
":")
(string-downcase (symbol-name form))))
(number (let ((*print-base* 10))
(princ-to-string form)))))
(defun eval-in-emacs (form &optional nowait)
"Eval FORM in Emacs.
`sly-enable-evaluate-in-emacs' should be set to T on the Emacs side."
(cond (nowait
(send-to-emacs `(:eval-no-wait ,(process-form-for-emacs form))))
(t
(force-output)
(let ((tag (make-tag)))
(send-to-emacs `(:eval ,(current-thread-id) ,tag
,(process-form-for-emacs form)))
(let ((value (caddr (wait-for-event `(:emacs-return ,tag result)))))
(destructure-case value
((:ok value) value)
((:error kind . data) (error "~a: ~{~a~}" kind data))
((:abort) (abort))))))))
(defun sly-version-string ()
"Return a string identifying the SLY version.
Return nil if nothing appropriate is available."
(let ((this-file #.(or *compile-file-truename* *load-truename*)))
(with-open-file (s (make-pathname :name "sly" :type "el"
:directory (butlast
(pathname-directory this-file)
1)
:defaults this-file))
(let ((seq (make-array 200 :element-type 'character :initial-element #\null)))
(read-sequence seq s :end 200)
(let* ((beg (search ";; Version:" seq))
(end (position #\NewLine seq :start beg))
(middle (position #\Space seq :from-end t :end end)))
(subseq seq (1+ middle) end))))))
(defvar *slynk-wire-protocol-version* (ignore-errors (sly-version-string))
"The version of the slynk/sly communication protocol.")
(defslyfun connection-info ()
"Return a key-value list of the form:
\(&key PID STYLE LISP-IMPLEMENTATION MACHINE FEATURES PACKAGE VERSION)
PID: is the process-id of Lisp process (or nil, depending on the STYLE)
STYLE: the communication style
LISP-IMPLEMENTATION: a list (&key TYPE NAME VERSION PROGRAM)
FEATURES: a list of keywords
PACKAGE: a list (&key NAME PROMPT)
VERSION: the protocol version"
(let ((c *emacs-connection*))
(setq *sly-features* *features*)
`(:pid ,(getpid) :style ,(connection-communication-style c)
:encoding (:coding-systems
,(loop for cs in '("utf-8-unix" "iso-latin-1-unix")
when (find-external-format cs) collect cs))
:lisp-implementation (:type ,(lisp-implementation-type)
:name ,(lisp-implementation-type-name)
:version ,(lisp-implementation-version)
:program ,(lisp-implementation-program))
:machine (:instance ,(machine-instance)
:type ,(machine-type)
:version ,(machine-version))
:features ,(features-for-emacs)
:modules ,*modules*
:package (:name ,(package-name *package*)
:prompt ,(package-string-for-prompt *package*))
:version ,*slynk-wire-protocol-version*)))
(defun debug-on-slynk-error ()
(assert (eq *debug-on-slynk-protocol-error* *debug-slynk-backend*))
*debug-on-slynk-protocol-error*)
(defun (setf debug-on-slynk-error) (new-value)
(setf *debug-on-slynk-protocol-error* new-value)
(setf *debug-slynk-backend* new-value))
(defslyfun toggle-debug-on-slynk-error ()
(setf (debug-on-slynk-error) (not (debug-on-slynk-error))))
(defvar-unbound *buffer-package*
"Package corresponding to sly-buffer-package.
EVAL-FOR-EMACS binds *buffer-package*. Strings originating from a sly
buffer are best read in this package. See also FROM-STRING and TO-STRING.")
(defvar-unbound *buffer-readtable*
"Readtable associated with the current buffer")
(defmacro with-buffer-syntax ((&optional package-designator
readtable)
&body body)
"Conceptually execute BODY inside a SLY Lisp buffer.
Execute BODY with appropriate *PACKAGE* and *READTABLE* bindings.
PACKAGE-DESIGNATOR, if non-NIL, is anything remotely designating a
package. READTABLE, if non-NIL, must verify CL:READTABLEP.
READTABLE defaults to *BUFFER-READTABLE* as set by
GUESS-BUFFER-READTABLE, which in turn uses a mapping in
*READTABLE-ALIST* as indexed by *BUFFER-PACKAGE*, and *not*
PACKAGE-DESIGNATOR.
This should be used for code that is conceptionally executed in an
Emacs buffer."
`(call-with-buffer-syntax ,package-designator ,readtable (lambda () ,@body)))
(defun call-with-buffer-syntax (package readtable fun)
(let ((*package* (if package
(guess-buffer-package package)
*buffer-package*))
(*buffer-readtable* (or (and (readtablep readtable)
readtable)
*buffer-readtable*)))
(if (eq *readtable* *buffer-readtable*)
(call-with-syntax-hooks fun)
(let ((*readtable* *buffer-readtable*))
(call-with-syntax-hooks fun)))))
(defmacro without-printing-errors ((&key object stream
(msg "<<error printing object>>"))
&body body)
"Catches errors during evaluation of BODY and prints MSG instead."
`(handler-case (progn ,@body)
(serious-condition ()
,(cond ((and stream object)
(let ((gstream (gensym "STREAM+")))
`(let ((,gstream ,stream))
(print-unreadable-object (,object ,gstream :type t
:identity t)
(write-string ,msg ,gstream)))))
(stream
`(write-string ,msg ,stream))
(object
`(with-output-to-string (s)
(print-unreadable-object (,object s :type t :identity t)
(write-string ,msg s))))
(t msg)))))
(defun to-string (object)
"Write OBJECT in the *BUFFER-PACKAGE*.
The result may not be readable. Handles problems with PRINT-OBJECT methods
gracefully."
(with-buffer-syntax ()
(let ((*print-readably* nil))
(without-printing-errors (:object object :stream nil)
(prin1-to-string object)))))
(defun from-string (string)
"Read string in the *BUFFER-PACKAGE*"
(with-buffer-syntax ()
(let ((*read-suppress* nil))
(values (read-from-string string)))))
(defun parse-string (string package)
"Read STRING in PACKAGE."
(with-buffer-syntax (package)
(let ((*read-suppress* nil))
(read-from-string string))))
(defun tokenize-symbol (string)
"STRING is interpreted as the string representation of a symbol
and is tokenized accordingly. The result is returned in three
values: The package identifier part, the actual symbol identifier
part, and a flag if the STRING represents a symbol that is
internal to the package identifier part. (Notice that the flag is
also true with an empty package identifier part, as the STRING is
considered to represent a symbol internal to some current package.)"
(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 (not (= (count #\: string) 1))))
(values symbol package internp)))
(defun tokenize-symbol-thoroughly (string)
"This version of TOKENIZE-SYMBOL handles escape characters."
(let ((package nil)
(token (make-array (length string) :element-type 'character
:fill-pointer 0))
(backslash nil)
(vertical nil)
(internp nil)
(caser (char-casifier string)))
(loop for char across string do
(cond
(backslash
(vector-push-extend char token)
(setq backslash nil))
((char= char #\\) (setq backslash t))
((char= char #\|)
(setq vertical (not vertical)))
(vertical
(vector-push-extend char token))
((char= char #\:)
(cond ((and package internp)
(return-from tokenize-symbol-thoroughly))
(package
(setq internp t))
(t
(setq package token
token (make-array (length string)
:element-type 'character
:fill-pointer 0)))))
(t
(vector-push-extend (funcall caser char) token))))
(unless vertical
(values token package (or (not package) internp)))))
(defun untokenize-symbol (package-name internal-p symbol-name)
"The inverse of TOKENIZE-SYMBOL.
(untokenize-symbol \"quux\" nil \"foo\") ==> \"quux:foo\"
(untokenize-symbol \"quux\" t \"foo\") ==> \"quux::foo\"
(untokenize-symbol nil nil \"foo\") ==> \"foo\"
"
(cond ((not package-name) symbol-name)
(internal-p (cat package-name "::" symbol-name))
(t (cat package-name ":" symbol-name))))
(defun char-casifier (string)
"Return a function which converts characters in STRING according to `readtable-case'."
(ecase (readtable-case *readtable*)
(:preserve #'identity)
(:upcase #'char-upcase)
(:downcase #'char-downcase)
(:invert (let ((upper (count-if #'upper-case-p string)))
(cond ((= upper 0) #'char-upcase)
((= upper (length string)) #'char-downcase)
(t #'identity))))))
(defun find-symbol-with-status (symbol-name status
&optional (package *package*))
(multiple-value-bind (symbol flag) (find-symbol symbol-name package)
(if (and flag (eq flag status))
(values symbol flag)
(values nil nil))))
(defun parse-symbol (string &optional (package *package*))
"Find the symbol named STRING.
Return the symbol and a flag indicating whether the symbols was found."
(multiple-value-bind (sname pname internalp)
(tokenize-symbol-thoroughly string)
(when sname
(let ((package (cond ((string= pname "") +keyword-package+)
(pname (find-package pname))
(t package))))
(if package
(multiple-value-bind (symbol flag)
(if internalp
(find-symbol sname package)
(find-symbol-with-status sname ':external package))
(values symbol flag sname package))
(values nil nil nil nil))))))
(defun parse-symbol-or-lose (string &optional (package *package*))
(multiple-value-bind (symbol status) (parse-symbol string package)
(if status
(values symbol status)
(error "Unknown symbol: ~A [in ~A]" string package))))
(defun parse-package (string)
"Find the package named STRING.
Return the package or nil."
(ignore-errors
(find-package (let ((*package* *slynk-io-package*))
(read-from-string string)))))
(defun unparse-name (string)
"Print the name STRING according to the current printer settings."
(subseq (prin1-to-string (make-symbol string)) 2))
(defun guess-package (string)
"Guess which package corresponds to STRING.
Return nil if no package matches."
(when string
(or (find-package string)
(parse-package string)
(if (find #\! string) (guess-package (substitute #\- #\! string))))))
(defvar *readtable-alist* (default-readtable-alist)
"An alist mapping package names to readtables.")
(defun guess-buffer-readtable (package-name)
(let ((package (guess-package package-name)))
(or (and package
(cdr (assoc (package-name package) *readtable-alist*
:test #'string=)))
*readtable*)))
(defvar *pending-continuations* '()
"List of continuations for Emacs. (thread local)")
(defun guess-buffer-package (string)
"Return a package for STRING.
Fall back to the current if no such package exists."
(or (and string (guess-package string))
*package*))
(defvar *eval-for-emacs-wrappers* nil
"List of functions for fine-grained control over form evaluation.
Each element must be a function taking an arbitrary number of
arguments, the first of which is a function of no arguments, call it
IN-FUNCTION, while the remaining are bound to the EXTRA-REX-OPTIONS
parameter of EVAL-FOR-EMACS. Every function *must* return another
function of no arguments, call it OUT-FUNCTION, that, when called,
*must* call IN-FUNCTION in whatever dynamic environment it sees fit.
Slynk will go through the elements of this variable in order, passing
a function that evaluates the form coming from Emacs to the first
element until it collects the result of the last, which is finally
called with no arguments.
Be careful when changing this variable since you may mess very basic
functionality of your Slynk, including the ability to correct any
errors you make.")
(defun eval-for-emacs (form buffer-package id &rest extra-rex-options)
"Bind *BUFFER-PACKAGE* to BUFFER-PACKAGE and evaluate FORM.
Return the result to the continuation ID. Errors are trapped and
invoke our debugger. EXTRA-REX-OPTIONS are passed to the functions of
*EVAL-FOR-EMACS-WRAPPERS*, which see."
(let (ok result condition)
(unwind-protect
(let ((*buffer-package* (guess-buffer-package buffer-package))
(*buffer-readtable* (guess-buffer-readtable buffer-package))
(*pending-continuations* (cons id *pending-continuations*)))
(check-type *buffer-package* package)
(check-type *buffer-readtable* readtable)
(handler-bind ((t (lambda (c) (setf condition c))))
(setq result (with-sly-interrupts
(flet ((eval-it ()
(eval form)))
(loop for lambda = #'eval-it then
(handler-case
(apply wrapper lambda extra-rex-options)
(error (e)
(warn "~s ignoring wrapper ~a (~a)"
'eval-for-emacs wrapper e)
lambda))
for wrapper in *eval-for-emacs-wrappers*
finally (return (funcall lambda)))))))
(run-hook *pre-reply-hook*)
(setq ok t))
(send-to-emacs `(:return ,(current-thread)
,(if ok
`(:ok ,result)
`(:abort ,(prin1-to-string condition)))
,id)))))
(defun format-integer-length (i) (format nil "~a bit~:p" (integer-length i)))
(defun format-integer-as-hex (i)
(unless (or (minusp i) (> (integer-length i) 64)) (format nil "#x~X" i)))
(defun format-integer-as-octal (i)
(unless (or (minusp i) (> (integer-length i) 8)) (format nil "#o~O" i)))
(defun format-integer-as-binary (i) -128
(unless (or (minusp i) (> (integer-length i) 8)) (format nil "#b~B" i)))
(defun format-ratio-as-float (r) (ignore-errors (format nil "~f" r)))
(defun format-as-percentage-maybe (f) (when (< 0 (abs f) 2) (format nil "~2,'0d%" (* f 100))))
(defparameter *echo-number-alist*
'((integer . (format-integer-length format-integer-as-hex format-integer-as-octal format-integer-as-binary))
(ratio . (format-ratio-as-float format-as-percentage-maybe))
(float . (format-as-percentage-maybe)))
"Alist of functions used for presenting numbers in the echo area.
Each element takes the form (TYPE . FUNCTIONS), where TYPE is a type
designator and FUNCTIONS is a list of function designators for
displaying that number in SLY. Each function takes the number as a
single argument and returns a string, or nil, if that particular
representation is to be disregarded.
Additionally if a given function chooses to return t as its optional
second value, then all the remaining functions following it in the
list are disregarded.")
(defparameter *present-number-alist* nil
"Alist of functions used for presenting numbers the REPL.
This is an \"override\". If nil the (the alist is empty) the value of
*ECHO-NUMBER-ALIST* is used, otherwise the structure is exactly the
same as that variable.")
(defun present-number-considering-alist (number alist)
(let* ((functions (cdr (assoc number alist :test #'typep)))
(extra-presentations
(loop for fn in functions
for (display skip)
= (multiple-value-list
(handler-case
(funcall fn number)
(error (e)
(declare (ignore e))
"<error echoing>")))
when display collect it
until skip)))
(if extra-presentations
(format nil "~A (~{~a~^, ~})"
number extra-presentations)
(format nil "~A" number))))
(defun echo-for-emacs (values &optional (fn #'slynk-pprint))
"Format VALUES in a way suitable to be echoed in the SLY client.
May insert newlines between each of VALUES. Considers
*ECHO-NUMBER-ALIST*."
(let ((*print-readably* nil))
(cond ((null values) "; No value")
((and (numberp (car values))
(null (cdr values)))
(present-number-considering-alist (car values) *echo-number-alist*))
(t
(let ((strings (loop for v in values
collect (funcall fn v))))
(if (some #'(lambda (s) (find #\Newline s))
strings)
(format nil "~{~a~^~%~}" strings)
(format nil "~{~a~^, ~}" strings)))))))
(defun present-for-emacs (value &optional (fn #'slynk-pprint))
"Format VALUE in a way suitable to be displayed in the SLY client.
FN is only used if value is not a number"
(if (numberp value)
(present-number-considering-alist value (or *present-number-alist*
*echo-number-alist*))
(funcall fn value)))
(defslyfun interactive-eval (string)
(with-buffer-syntax ()
(with-retry-restart (:msg "Retry SLY interactive evaluation request.")
(let ((values (multiple-value-list (eval (from-string string)))))
(finish-output)
(echo-for-emacs values)))))
(defslyfun eval-and-grab-output (string)
(with-buffer-syntax ()
(with-retry-restart (:msg "Retry SLY evaluation request.")
(let* ((s (make-string-output-stream))
(*standard-output* s)
(values (multiple-value-list (eval (from-string string)))))
(list (get-output-stream-string s)
(echo-for-emacs values))))))
(defun eval-region (string)
"Evaluate STRING.
Return the results of the last form as a list and as secondary value the
last form."
(with-input-from-string (stream string)
(let (- values)
(loop
(let ((form (read stream nil stream)))
(when (eq form stream)
(finish-output)
(return (values values -)))
(setq - form)
(setq values (multiple-value-list (eval form)))
(finish-output))))))
(defslyfun interactive-eval-region (string)
(with-buffer-syntax ()
(with-retry-restart (:msg "Retry SLY interactive evaluation request.")
(echo-for-emacs (eval-region string)))))
(defslyfun re-evaluate-defvar (form)
(with-buffer-syntax ()
(with-retry-restart (:msg "Retry SLY evaluation request.")
(let ((form (read-from-string form)))
(destructuring-bind (dv name &optional value doc) form
(declare (ignore value doc))
(assert (eq dv 'defvar))
(makunbound name)
(prin1-to-string (eval form)))))))
(defvar-unbound *string-elision-length*
"Maximum length of a sring before elision by SLYNK-PPRINT.")
(defparameter *slynk-pprint-bindings*
`((*print-pretty* . t)
(*print-level* . nil)
(*print-length* . nil)
(*string-elision-length* . 200)
(*print-circle* . nil)
(*print-gensym* . t)
(*print-readably* . nil))
"A list of variables bindings during pretty printing.
Used by pprint-eval.")
(defun slynk-pprint (object &key (stream nil))
"Pretty print OBJECT to STREAM using *SLYNK-PPRINT-BINDINGS*.
If STREAM is nil, use a string"
(with-bindings *slynk-pprint-bindings*
(let ((*print-length* (or *print-length*
(and (not *print-circle*) 512)))
(*print-level* (or *print-level*
(and (not *print-circle*) 20))))
(flet ((write-it (s)
(cond ((and *string-elision-length*
(stringp object)
(> (length object) *string-elision-length*))
(format s "\"~a...[sly-elided string of length ~a]\""
(subseq object 0 *string-elision-length*)
(length object)))
(t
(write object :stream s :pretty t :escape t)))))
(if stream
(without-printing-errors (:object object :stream stream)
(write-it stream))
(without-printing-errors (:object object)
(with-output-to-string (s) (write-it s))))))))
(defun slynk-pprint-values (values &key (stream nil))
"Pretty print each of VALUES to STREAM using *SLYNK-PPRINT-BINDINGS*.
Separated by a newline. If no values indicate that in a comment.
If STREAM is nil, use a string"
(labels ((print-one (object s)
(let ((*slynk-pprint-bindings* nil))
(slynk-pprint object :stream s)))
(print-all (s)
(loop for o in values
do (print-one o s)
(terpri))))
(with-bindings *slynk-pprint-bindings*
(cond ((null values)
(format stream "; No value"))
(t
(if stream
(print-all stream)
(with-output-to-string (s)
(print-all s))))))))
(defun slynk-pprint-to-line (object)
"Print OBJECT to a single line string and return it."
(let ((*slynk-pprint-bindings*
`((*print-lines* . 1)
(*print-right-margin* . 512)
,@*slynk-pprint-bindings*)))
(substitute #\Space #\Newline (slynk-pprint object :stream nil))))
(defslyfun pprint-eval (string)
(with-buffer-syntax ()
(let* ((s (make-string-output-stream))
(values
(let ((*standard-output* s)
(*trace-output* s))
(multiple-value-list (eval (read-from-string string))))))
(cat (get-output-stream-string s)
(slynk-pprint-values values)))))
(defslyfun set-package (name)
"Set *package* to the package named NAME.
Return the full package-name and the string to use in the prompt."
(let ((p (guess-package name)))
(assert (packagep p) nil "Package ~a doesn't exist." name)
(setq *package* p)
(list (package-name p) (package-string-for-prompt p))))
(defun cat (&rest strings)
"Concatenate all arguments and make the result a string."
(with-output-to-string (out)
(dolist (s strings)
(etypecase s
(string (write-string s out))
(character (write-char s out))))))
(defun truncate-string (string width &optional ellipsis)
(let ((len (length string)))
(cond ((< len width) string)
(ellipsis (cat (subseq string 0 width) ellipsis))
(t (subseq string 0 width)))))
(defun call/truncated-output-to-string (length function
&optional (ellipsis ".."))
"Call FUNCTION with a new stream, return the output written to the stream.
If FUNCTION tries to write more than LENGTH characters, it will be
aborted and return immediately with the output written so far."
(let ((buffer (make-string (+ length (length ellipsis))))
(fill-pointer 0))
(block buffer-full
(flet ((write-output (string)
(let* ((free (- length fill-pointer))
(count (min free (length string))))
(replace buffer string :start1 fill-pointer :end2 count)
(incf fill-pointer count)
(when (> (length string) free)
(replace buffer ellipsis :start1 fill-pointer)
(return-from buffer-full buffer)))))
(let ((stream (make-output-stream #'write-output)))
(funcall function stream)
(finish-output stream)
(subseq buffer 0 fill-pointer))))))
(defmacro with-string-stream ((var &key length bindings)
&body body)
(cond ((and (not bindings) (not length))
`(with-output-to-string (,var) . ,body))
((not bindings)
`(call/truncated-output-to-string
,length (lambda (,var) . ,body)))
(t
`(with-bindings ,bindings
(with-string-stream (,var :length ,length)
. ,body)))))
(defun escape-string (string stream &key length (map '((#\" . "\\\"")
(#\\ . "\\\\"))))
"Write STRING to STREAM surronded by double-quotes.
LENGTH -- if non-nil truncate output after LENGTH chars.
MAP -- rewrite the chars in STRING according to this alist."
(let ((limit (or length array-dimension-limit)))
(write-char #\" stream)
(loop for c across string
for i from 0 do
(when (= i limit)
(write-string "..." stream)
(return))
(let ((probe (assoc c map)))
(cond (probe (write-string (cdr probe) stream))
(t (write-char c stream)))))
(write-char #\" stream)))
(defvar *canonical-package-nicknames*
`((:common-lisp-user . :cl-user))
"Canonical package names to use instead of shortest name/nickname.")
(defvar *auto-abbreviate-dotted-packages* t
"Abbreviate dotted package names to their last component if T.")
(defun package-string-for-prompt (package)
"Return the shortest nickname (or canonical name) of PACKAGE."
(unparse-name
(or (canonical-package-nickname package)
(auto-abbreviated-package-name package)
(shortest-package-nickname package))))
(defun canonical-package-nickname (package)
"Return the canonical package nickname, if any, of PACKAGE."
(let ((name (cdr (assoc (package-name package) *canonical-package-nicknames*
:test #'string=))))
(and name (string name))))
(defun auto-abbreviated-package-name (package)
"Return an abbreviated 'name' for PACKAGE.
N.B. this is not an actual package name or nickname."
(when *auto-abbreviate-dotted-packages*
(loop with package-name = (package-name package)
with offset = nil
do (let ((last-dot-pos (position #\. package-name :end offset
:from-end t)))
(unless last-dot-pos
(return nil))
(let ((name (subseq package-name (1+ last-dot-pos) offset)))
(if (notevery #'digit-char-p name)
(return (subseq package-name (1+ last-dot-pos)))
(setq offset last-dot-pos)))))))
(defun shortest-package-nickname (package)
"Return the shortest nickname of PACKAGE."
(loop for name in (cons (package-name package) (package-nicknames package))
for shortest = name then (if (< (length name) (length shortest))
name
shortest)
finally (return shortest)))
(defslyfun ed-in-emacs (&optional what)
"Edit WHAT in Emacs.
WHAT can be:
A pathname or a string,
A list (PATHNAME-OR-STRING &key LINE COLUMN POSITION),
A function name (symbol or cons),
NIL. "
(flet ((canonicalize-filename (filename)
(pathname-to-filename (or (probe-file filename) filename))))
(let ((target
(etypecase what
(null nil)
((or string pathname)
`(:filename ,(canonicalize-filename what)))
((cons (or string pathname) *)
`(:filename ,(canonicalize-filename (car what)) ,@(cdr what)))
((or symbol cons)
`(:function-name ,(prin1-to-string what))))))
(cond (*emacs-connection* (send-oob-to-emacs `(:ed ,target)))
((default-connection)
(with-connection ((default-connection))
(send-oob-to-emacs `(:ed ,target))))
(t (error "No connection"))))))
(defslyfun inspect-in-emacs (what &key wait)
"Inspect WHAT in Emacs. If WAIT is true (default NIL) blocks until the
inspector has been closed in Emacs."
(flet ((send-it ()
(let ((tag (when wait (make-tag)))
(thread (when wait (current-thread-id))))
(with-buffer-syntax ()
(reset-inspector)
(send-oob-to-emacs `(:inspect ,(inspect-object what)
,thread
,tag)))
(when wait
(wait-for-event `(:emacs-return ,tag result))))))
(cond
(*emacs-connection*
(send-it))
((default-connection)
(with-connection ((default-connection))
(send-it))))
what))
(defslyfun value-for-editing (form)
"Return a readable value of FORM for editing in Emacs.
FORM is expected, but not required, to be SETF'able."
(with-buffer-syntax ()
(let* ((value (eval (read-from-string form)))
(*print-length* nil))
(prin1-to-string value))))
(defslyfun commit-edited-value (form value)
"Set the value of a setf'able FORM to VALUE.
FORM and VALUE are both strings from Emacs."
(with-buffer-syntax ()
(eval `(setf ,(read-from-string form)
,(read-from-string (concatenate 'string "`" value))))
t))
(defun background-message (format-string &rest args)
"Display a message in Emacs' echo area.
Use this function for informative messages only. The message may even
be dropped if we are too busy with other things."
(when *emacs-connection*
(send-to-emacs `(:background-message
,(apply #'format nil format-string args)))))
(defun sleep-for (seconds)
"Sleep for at least SECONDS seconds.
This is just like cl:sleep but guarantees to sleep
at least SECONDS."
(let* ((start (get-internal-real-time))
(end (+ start
(* seconds internal-time-units-per-second))))
(loop
(let ((now (get-internal-real-time)))
(cond ((< end now) (return))
(t (sleep (/ (- end now)
internal-time-units-per-second))))))))
(defun invoke-sly-debugger (condition)
"Sends a message to Emacs declaring that the debugger has been entered,
then waits to handle further requests from Emacs. Eventually returns
after Emacs causes a restart to be invoked."
(without-sly-interrupts
(cond (*emacs-connection*
(debug-in-emacs condition))
((default-connection)
(with-connection ((default-connection))
(debug-in-emacs condition))))))
(define-condition invoke-default-debugger () ())
(defun slynk-debugger-hook (condition hook)
"Debugger function for binding *DEBUGGER-HOOK*."
(declare (ignore hook))
(handler-case
(call-with-debugger-hook #'slynk-debugger-hook
(lambda () (invoke-sly-debugger condition)))
(invoke-default-debugger ()
(invoke-default-debugger condition))))
(defun invoke-default-debugger (condition)
(call-with-debugger-hook nil (lambda () (invoke-debugger condition))))
(defvar *global-debugger* t
"Non-nil means the Slynk debugger hook will be installed globally.")
(add-hook *new-connection-hook* 'install-debugger)
(defun install-debugger (connection)
(declare (ignore connection))
(when *global-debugger*
(install-debugger-globally #'slynk-debugger-hook)))
(defvar *slynk-debugger-condition* nil
"The condition being debugged.")
(defvar *sly-db-level* 0
"The current level of recursive debugging.")
(defvar *sly-db-initial-frames* 20
"The initial number of backtrace frames to send to Emacs.")
(defvar *sly-db-restarts* nil
"The list of currenlty active restarts.")
(defvar *sly-db-stepping-p* nil
"True during execution of a step command.")
(defun debug-in-emacs (condition)
(let ((*slynk-debugger-condition* condition)
(*sly-db-restarts* (compute-restarts condition))
(*sly-db-quit-restart* (and *sly-db-quit-restart*
(find-restart *sly-db-quit-restart*
condition)))
(*package* (or (and (boundp '*buffer-package*)
(symbol-value '*buffer-package*))
*package*))
(*sly-db-level* (1+ *sly-db-level*))
(*sly-db-stepping-p* nil))
(force-user-output)
(call-with-debugging-environment
(lambda ()
(sly-db-loop *sly-db-level*)))))
(defun sly-db-loop (level)
(unwind-protect
(loop
(with-simple-restart (abort "Return to sly-db level ~D." level)
(send-to-emacs
(list* :debug (current-thread-id) level
(debugger-info-for-emacs 0 *sly-db-initial-frames*)))
(send-to-emacs
(list :debug-activate (current-thread-id) level))
(loop
(handler-case
(destructure-case (wait-for-event
`(or (:emacs-rex . _)
(:emacs-channel-send . _)
(:sly-db-return ,(1+ level))))
((:emacs-rex &rest args) (apply #'eval-for-emacs args))
((:emacs-channel-send channel (selector &rest args))
(channel-send channel selector args))
((:sly-db-return _) (declare (ignore _)) (return nil)))
(sly-db-condition (c)
(handle-sly-db-condition c))))))
(send-to-emacs `(:debug-return
,(current-thread-id) ,level ,*sly-db-stepping-p*))
(wait-for-event `(:sly-db-return ,(1+ level)) t) (when (> level 1)
(send-event (current-thread) `(:sly-db-return ,level)))))
(defun handle-sly-db-condition (condition)
"Handle an internal debugger condition.
Rather than recursively debug the debugger (a dangerous idea!), these
conditions are simply reported."
(let ((real-condition (original-condition condition)))
(send-to-emacs `(:debug-condition ,(current-thread-id)
,(princ-to-string real-condition)))))
(defun %%condition-message (condition)
(let ((limit (ash 1 16)))
(with-string-stream (stream :length limit)
(handler-case
(let ((*print-readably* nil)
(*print-pretty* t)
(*print-right-margin* 65)
(*print-circle* t)
(*print-length* (or *print-length* limit))
(*print-level* (or *print-level* limit))
(*print-lines* (or *print-lines* limit)))
(print-condition condition stream))
(serious-condition (c)
(ignore-errors
(with-standard-io-syntax
(let ((*print-readably* nil))
(format stream "~&Error (~a) printing the following condition: " (type-of c))
(print-unreadable-object (condition stream :type t
:identity t))))))))))
(defun %condition-message (condition)
(string-trim #(#\newline #\space #\tab)
(%%condition-message condition)))
(defvar *sly-db-condition-printer* #'%condition-message
"Function called to print a condition to an SLY-DB buffer.")
(defun safe-condition-message (condition)
"Print condition to a string, handling any errors during printing."
(funcall *sly-db-condition-printer* condition))
(defvar *debugger-extra-options* nil
"A property list of extra options describing a condition.
This works much like the CONDITION-EXTRAS interface, but can be
dynamically bound by contribs when invoking the debugger.")
(defun debugger-condition-for-emacs ()
(list (safe-condition-message *slynk-debugger-condition*)
(format nil " [Condition of type ~S]"
(type-of *slynk-debugger-condition*))
(append (condition-extras *slynk-debugger-condition*)
*debugger-extra-options*)))
(defun format-restarts-for-emacs ()
"Return a list of restarts for *slynk-debugger-condition* in a
format suitable for Emacs."
(let ((*print-right-margin* most-positive-fixnum))
(loop for restart in *sly-db-restarts* collect
(list (format nil "~:[~;*~]~a"
(eq restart *sly-db-quit-restart*)
(restart-name restart))
(with-output-to-string (stream)
(without-printing-errors (:object restart
:stream stream
:msg "<<error printing restart>>")
(princ restart stream)))))))
(defslyfun sly-db-break-with-default-debugger (dont-unwind)
"Invoke the default debugger."
(cond (dont-unwind
(invoke-default-debugger *slynk-debugger-condition*))
(t
(signal 'invoke-default-debugger))))
(defslyfun backtrace (start end)
"Return a list ((I FRAME PLIST) ...) of frames from START to END.
I is an integer, and can be used to reference the corresponding frame
from Emacs; FRAME is a string representation of an implementation's
frame."
(loop for frame in (compute-backtrace start end)
for i from start collect
(list* i (frame-to-string frame)
(ecase (frame-restartable-p frame)
((nil) nil)
((t) `((:restartable t)))))))
(defun frame-to-string (frame)
(with-string-stream (stream :length (* (or *print-lines* 1)
(or *print-right-margin* 100))
:bindings *backtrace-printer-bindings*)
(handler-case (print-frame frame stream)
(serious-condition ()
(format stream "[error printing frame]")))))
(defslyfun debugger-info-for-emacs (start end)
"Return debugger state, with stack frames from START to END.
The result is a list:
(condition ({restart}*) ({stack-frame}*) (cont*))
where
condition ::= (description type [extra])
restart ::= (name description)
stack-frame ::= (number description [plist])
extra ::= (:references and other random things)
cont ::= continuation
plist ::= (:restartable {nil | t | :unknown})
condition---a pair of strings: message, and type. If show-source is
not nil it is a frame number for which the source should be displayed.
restart---a pair of strings: restart name, and description.
stack-frame---a number from zero (the top), and a printed
representation of the frame's call.
continuation---the id of a pending Emacs continuation.
Below is an example return value. In this case the condition was a
division by zero (multi-line description), and only one frame is being
fetched (start=0, end=1).
((\"Arithmetic error DIVISION-BY-ZERO signalled.
Operation was KERNEL::DIVISION, operands (1 0).\"
\"[Condition of type DIVISION-BY-ZERO]\")
((\"ABORT\" \"Return to Sly toplevel.\")
(\"ABORT\" \"Return to Top-Level.\"))
((0 \"(KERNEL::INTEGER-/-INTEGER 1 0)\" (:restartable nil)))
(4))"
(list (debugger-condition-for-emacs)
(format-restarts-for-emacs)
(backtrace start end)
*pending-continuations*))
(defun nth-restart (index)
(nth index *sly-db-restarts*))
(defslyfun invoke-nth-restart (index)
(let ((restart (nth-restart index)))
(when restart
(let* ((prompt nil)
(*query-io*
(make-two-way-stream
(make-input-stream
(lambda ()
(format nil "~a~%"
(read-from-minibuffer-in-emacs
(format nil "~a" (or prompt
"[restart prompt] :"))))))
(make-output-stream
#'(lambda (s)
(setq prompt
(concatenate 'string
(or prompt "")
s)))))))
(invoke-restart-interactively restart)))))
(defslyfun sly-db-abort ()
(invoke-restart (find 'abort *sly-db-restarts* :key #'restart-name)))
(defslyfun sly-db-continue ()
(continue))
(defun coerce-to-condition (datum args)
(etypecase datum
(string (make-condition 'simple-error :format-control datum
:format-arguments args))
(symbol (apply #'make-condition datum args))))
(defslyfun simple-break (&optional (datum "Interrupt from Emacs") &rest args)
(with-simple-restart (continue "Continue from break.")
(invoke-sly-debugger (coerce-to-condition datum args))))
(defslyfun throw-to-toplevel ()
"Invoke the ABORT-REQUEST restart abort an RPC from Emacs.
If we are not evaluating an RPC then ABORT instead."
(let ((restart (or (and *sly-db-quit-restart*
(find-restart *sly-db-quit-restart*))
(car (last (compute-restarts))))))
(cond (restart (invoke-restart restart))
(t (format nil "Restart not active [~s]" *sly-db-quit-restart*)))))
(defslyfun invoke-nth-restart-for-emacs (sly-db-level n)
"Invoke the Nth available restart.
SLY-DB-LEVEL is the debug level when the request was made. If this
has changed, ignore the request."
(when (= sly-db-level *sly-db-level*)
(invoke-nth-restart n)))
(defun wrap-sly-db-vars (form)
`(let ((*sly-db-level* ,*sly-db-level*))
,form))
(defun eval-in-frame-aux (frame string package print)
(let* ((form (wrap-sly-db-vars (parse-string string package)))
(values (multiple-value-list (eval-in-frame form frame))))
(with-buffer-syntax (package)
(funcall print values))))
(defslyfun eval-string-in-frame (string frame package)
(eval-in-frame-aux frame string package #'echo-for-emacs))
(defslyfun pprint-eval-string-in-frame (string frame package)
(eval-in-frame-aux frame string package #'slynk-pprint-values))
(defslyfun frame-package-name (frame)
(let ((pkg (frame-package frame)))
(cond (pkg (package-name pkg))
(t (with-buffer-syntax () (package-name *package*))))))
(defslyfun frame-locals-and-catch-tags (index)
"Return a list (LOCALS TAGS) for vars and catch tags in the frame INDEX.
LOCALS is a list of the form ((&key NAME ID VALUE) ...).
TAGS has is a list of strings."
(list (frame-locals-for-emacs index)
(mapcar #'to-string (frame-catch-tags index))))
(defun frame-locals-for-emacs (index)
(loop for var in (frame-locals index)
collect
(destructuring-bind (&key name id value) var
(list :name (let ((*package* (or (frame-package index) *package*)))
(prin1-to-string name))
:id id
:value
(let ((*slynk-pprint-bindings*
(append *slynk-pprint-bindings*
*backtrace-printer-bindings*)))
(slynk-pprint value))))))
(defslyfun sly-db-disassemble (index)
(with-output-to-string (*standard-output*)
(disassemble-frame index)))
(defslyfun sly-db-return-from-frame (index string)
(let ((form (from-string string)))
(to-string (multiple-value-list (return-from-frame index form)))))
(defslyfun sly-db-break (name)
(with-buffer-syntax ()
(sly-db-break-at-start (read-from-string name))))
(defmacro define-stepper-function (name backend-function-name)
`(defslyfun ,name (frame)
(cond ((sly-db-stepper-condition-p *slynk-debugger-condition*)
(setq *sly-db-stepping-p* t)
(,backend-function-name))
((find-restart 'continue)
(activate-stepping frame)
(setq *sly-db-stepping-p* t)
(continue))
(t
(error "Not currently single-stepping, ~
and no continue restart available.")))))
(define-stepper-function sly-db-step sly-db-step-into)
(define-stepper-function sly-db-next sly-db-step-next)
(define-stepper-function sly-db-out sly-db-step-out)
(defslyfun toggle-break-on-signals ()
(setq *break-on-signals* (not *break-on-signals*))
(format nil "*break-on-signals* = ~a" *break-on-signals*))
(defslyfun sdlb-print-condition ()
(princ-to-string *slynk-debugger-condition*))
(defstruct (compilation-result (:type list))
(type :compilation-result)
notes
(successp nil :type boolean)
(duration 0.0 :type float)
(loadp nil :type boolean)
(faslfile nil :type (or null string)))
(defun measure-time-interval (fun)
"Call FUN and return the first return value and the elapsed time.
The time is measured in seconds."
(declare (type function fun))
(let ((before (get-internal-real-time))) (values
(funcall fun)
(/ (- (get-internal-real-time) before)
(coerce internal-time-units-per-second 'float)))))
(defun make-compiler-note (condition)
"Make a compiler note data structure from a compiler-condition."
(declare (type compiler-condition condition))
(list* :message (message condition)
:severity (severity condition)
:location (location condition)
:references (references condition)
(let ((s (source-context condition)))
(if s (list :source-context s)))))
(defun collect-notes (function)
(let ((notes '()))
(multiple-value-bind (result seconds)
(handler-bind ((compiler-condition
(lambda (c) (push (make-compiler-note c) notes))))
(measure-time-interval
(lambda ()
(restart-case (multiple-value-list (funcall function))
(abort () :report "Abort compilation." (list nil))))))
(destructuring-bind (successp &optional loadp faslfile) result
(let ((faslfile (etypecase faslfile
(null nil)
(pathname (pathname-to-filename faslfile)))))
(make-compilation-result :notes (reverse notes)
:duration seconds
:successp (if successp t)
:loadp (if loadp t)
:faslfile faslfile))))))
(defun slynk-compile-file* (pathname load-p &rest options &key policy
&allow-other-keys)
(multiple-value-bind (output-pathname warnings? failure?)
(slynk-compile-file pathname
(fasl-pathname pathname options)
nil
(or (guess-external-format pathname)
:default)
:policy policy)
(declare (ignore warnings?))
(values t (not failure?) load-p output-pathname)))
(defvar *compile-file-for-emacs-hook* '(slynk-compile-file*))
(defslyfun compile-file-for-emacs (filename load-p &rest options)
"Compile FILENAME and, when LOAD-P, load the result.
Record compiler notes signalled as `compiler-condition's."
(with-buffer-syntax ()
(collect-notes
(lambda ()
(let ((pathname (filename-to-pathname filename))
(*compile-print* nil)
(*compile-verbose* t))
(loop for hook in *compile-file-for-emacs-hook*
do
(multiple-value-bind (tried success load? output-pathname)
(apply hook pathname load-p options)
(when tried
(return (values success load? output-pathname))))))))))
(defvar *fasl-pathname-function* nil
"In non-nil, use this function to compute the name for fasl-files.")
(defun pathname-as-directory (pathname)
(append (pathname-directory pathname)
(when (pathname-name pathname)
(list (file-namestring pathname)))))
(defun compile-file-output (file directory)
(make-pathname :directory (pathname-as-directory directory)
:defaults (compile-file-pathname file)))
(defun fasl-pathname (input-file options)
(cond (*fasl-pathname-function*
(funcall *fasl-pathname-function* input-file options))
((getf options :fasl-directory)
(let ((dir (getf options :fasl-directory)))
(assert (char= (aref dir (1- (length dir))) #\/))
(compile-file-output input-file dir)))
(t
(compile-file-pathname input-file))))
(defslyfun compile-string-for-emacs (string buffer position filename policy)
"Compile STRING (exerpted from BUFFER at POSITION).
Record compiler notes signalled as `compiler-condition's."
(let* ((offset (cadr (assoc :position position)))
(line-column (cdr (assoc :line position)))
(line (first line-column))
(column (second line-column)))
(with-buffer-syntax ()
(collect-notes
(lambda ()
(let ((*compile-print* nil)
(*compile-verbose* nil)
(*load-verbose* nil))
(slynk-compile-string string
:buffer buffer
:position offset
:filename filename
:line line
:column column
:policy policy)))))))
(defslyfun compile-multiple-strings-for-emacs (strings policy)
"Compile STRINGS (exerpted from BUFFER at POSITION).
Record compiler notes signalled as `compiler-condition's."
(loop for (string buffer package position filename) in strings collect
(collect-notes
(lambda ()
(with-buffer-syntax (package)
(let ((*compile-print* t) (*compile-verbose* nil))
(slynk-compile-string string
:buffer buffer
:position position
:filename filename
:policy policy)))))))
(defun file-newer-p (new-file old-file)
"Returns true if NEW-FILE is newer than OLD-FILE."
(> (file-write-date new-file) (file-write-date old-file)))
(defun requires-compile-p (source-file)
(let ((fasl-file (probe-file (compile-file-pathname source-file))))
(or (not fasl-file)
(file-newer-p source-file fasl-file))))
(defslyfun compile-file-if-needed (filename loadp)
(let ((pathname (filename-to-pathname filename)))
(cond ((requires-compile-p pathname)
(compile-file-for-emacs pathname loadp))
(t
(collect-notes
(lambda ()
(or (not loadp)
(load (compile-file-pathname pathname)))))))))
(defslyfun load-file (filename)
(to-string (load (filename-to-pathname filename))))
(defvar *module-loading-method* (find-if #'find-package '(:slynk-loader :asdf))
"Keyword naming the module-loading method.
SLY's own `slynk-loader.lisp' is tried first, then ASDF")
(defvar *asdf-load-in-progress* nil
"Set to t if inside a \"ASDF:LOAD-SYSTEM\" operation.
Introduced to prevent problematic recursive ASDF loads, but going away
soon once non-ASDF loading is removed. (see github#134)")
(defgeneric require-module (method module)
(:documentation
"Use METHOD to load MODULE.
Receives a module name as argument and should return non-nil if it
managed to load it.")
(:method ((method (eql :slynk-loader)) module)
(funcall (intern "REQUIRE-MODULE" :slynk-loader) module))
(:method ((method (eql :asdf)) module)
(unless *asdf-load-in-progress*
(let ((*asdf-load-in-progress* t))
(funcall (intern "LOAD-SYSTEM" :asdf) module)))))
(defun add-to-load-path-1 (path load-path-var)
(pushnew path (symbol-value load-path-var) :test #'equal))
(defgeneric add-to-load-path (method path)
(:documentation
"Using METHOD, consider PATH when searching for modules.")
(:method ((method (eql :slynk-loader)) path)
(add-to-load-path-1 path (intern "*LOAD-PATH*" :slynk-loader)))
(:method ((method (eql :asdf)) path)
(add-to-load-path-1 path (intern "*CENTRAL-REGISTRY*" :asdf))))
(defvar *slynk-require-hook* '()
"Functions run after SLYNK-REQUIRE. Called with new modules.")
(defslyfun slynk-require (modules)
"Load each module in MODULES.
MODULES is a list of strings designators or a single string
designator. Returns a list of all modules available."
(let ((loaded))
(dolist (module (ensure-list modules))
(with-simple-restart (continue "Continue without SLY contrib ~a" module)
(funcall #'require-module *module-loading-method* module)
(push module loaded)
(pushnew (string-upcase module) *modules* :test #'equal))
(loop for fn in *slynk-require-hook*
do (funcall fn loaded)))
(list *modules* loaded)))
(defslyfun slynk-add-load-paths (paths)
(dolist (path paths)
(funcall #'add-to-load-path *module-loading-method* (pathname path))))
(defvar *macroexpand-printer-bindings*
'((*print-circle* . nil)
(*print-pretty* . t)
(*print-escape* . t)
(*print-lines* . nil)
(*print-level* . nil)
(*print-length* . nil)
(*print-case* . :downcase))
"Pretty-pretty bindings to use when expanding macros")
(defun apply-macro-expander (expander string)
(with-buffer-syntax ()
(let ((expansion (funcall expander (from-string string))))
(with-bindings *macroexpand-printer-bindings*
(prin1-to-string expansion)))))
(defslyfun slynk-macroexpand-1 (string)
(apply-macro-expander #'macroexpand-1 string))
(defslyfun slynk-macroexpand (string)
(apply-macro-expander #'macroexpand string))
(defslyfun slynk-macroexpand-all (string)
(apply-macro-expander #'macroexpand-all string))
(defslyfun slynk-compiler-macroexpand-1 (string)
(apply-macro-expander #'compiler-macroexpand-1 string))
(defslyfun slynk-compiler-macroexpand (string)
(apply-macro-expander #'compiler-macroexpand string))
(defslyfun slynk-expand-1 (string)
(apply-macro-expander #'expand-1 string))
(defslyfun slynk-expand (string)
(apply-macro-expander #'expand string))
(defun expand-1 (form)
(multiple-value-bind (expansion expanded?) (macroexpand-1 form)
(if expanded?
(values expansion t)
(compiler-macroexpand-1 form))))
(defun expand (form)
(expand-repeatedly #'expand-1 form))
(defun expand-repeatedly (expander form)
(loop
(multiple-value-bind (expansion expanded?) (funcall expander form)
(unless expanded? (return expansion))
(setq form expansion))))
(defslyfun slynk-format-string-expand (string)
(apply-macro-expander #'format-string-expand string))
(defslyfun disassemble-form (form)
(with-buffer-syntax ()
(with-output-to-string (*standard-output*)
(let ((*print-readably* nil))
(disassemble (eval (read-from-string form)))))))
(defslyfun operator-arglist (name package)
(ignore-errors
(let ((args (arglist (parse-symbol name (guess-buffer-package package)))))
(cond ((eq args :not-available) nil)
(t (princ-to-string (cons name args)))))))
(defun map-if (test fn &rest lists)
"Like (mapcar FN . LISTS) but only call FN on objects satisfying TEST.
Example:
\(map-if #'oddp #'- '(1 2 3 4 5)) => (-1 2 -3 4 -5)"
(apply #'mapcar
(lambda (x) (if (funcall test x) (funcall fn x) x))
lists))
(defun listify (f)
"Return a function like F, but which returns any non-null value
wrapped in a list."
(lambda (x)
(let ((y (funcall f x)))
(and y (list y)))))
(defun call-with-describe-settings (fn)
(let ((*print-readably* nil))
(funcall fn)))
(defmacro with-describe-settings ((&rest _) &body body)
(declare (ignore _))
`(call-with-describe-settings (lambda () ,@body)))
(defun describe-to-string (object)
(with-describe-settings ()
(with-output-to-string (*standard-output*)
(describe object))))
(defslyfun describe-symbol (symbol-name)
(with-buffer-syntax ()
(describe-to-string (parse-symbol-or-lose symbol-name))))
(defslyfun describe-function (name)
(with-buffer-syntax ()
(let ((symbol (parse-symbol-or-lose name)))
(describe-to-string (or (macro-function symbol)
(symbol-function symbol))))))
(defslyfun describe-definition-for-emacs (name kind)
(with-buffer-syntax ()
(with-describe-settings ()
(with-output-to-string (*standard-output*)
(describe-definition (parse-symbol-or-lose name) kind)))))
(defslyfun documentation-symbol (symbol-name)
(with-buffer-syntax ()
(multiple-value-bind (sym foundp) (parse-symbol symbol-name)
(if foundp
(let ((vdoc (documentation sym 'variable))
(fdoc (documentation sym 'function)))
(with-output-to-string (string)
(format string "Documentation for the symbol ~a:~2%" sym)
(unless (or vdoc fdoc)
(format string "Not documented." ))
(when vdoc
(format string "Variable:~% ~a~2%" vdoc))
(when fdoc
(format string "Function:~% Arglist: ~a~2% ~a"
(slynk-backend:arglist sym)
fdoc))))
(format nil "No such symbol, ~a." symbol-name)))))
(defslyfun list-all-package-names (&optional nicknames)
"Return a list of all package names.
Include the nicknames if NICKNAMES is true."
(mapcar #'unparse-name
(if nicknames
(mapcan #'package-names (list-all-packages))
(mapcar #'package-name (list-all-packages)))))
(defun tracedp (fspec)
(member fspec (eval '(trace))))
(defvar *after-toggle-trace-hook* nil
"Hook called whenever a SPEC is traced or untraced.
If non-nil, called with two arguments SPEC and TRACED-P." )
(defslyfun slynk-toggle-trace (spec-string)
(let* ((spec (from-string spec-string))
(retval (cond ((consp spec) (toggle-trace spec))
((tracedp spec)
(eval `(untrace ,spec))
(format nil "~S is now untraced." spec))
(t
(eval `(trace ,spec))
(format nil "~S is now traced." spec))))
(traced-p (let* ((tosearch "is now traced.")
(start (- (length retval)
(length tosearch)))
(end (+ start (length tosearch))))
(search tosearch (subseq retval start end))))
(hook-msg (when *after-toggle-trace-hook*
(funcall *after-toggle-trace-hook*
spec
traced-p))))
(if hook-msg
(format nil "~a~%(also ~a)" retval hook-msg)
retval)))
(defslyfun untrace-all ()
(untrace))
(defslyfun undefine-function (fname-string)
(let ((fname (from-string fname-string)))
(format nil "~S" (fmakunbound fname))))
(defun read-as-function (name)
(eval (from-string (format nil "(function ~A)" name))))
(defslyfun remove-method-by-name (generic-name qualifiers specializers)
"Remove GENERIC-NAME's method with QUALIFIERS and SPECIALIZERS."
(let* ((generic-function (read-as-function generic-name))
(qualifiers (mapcar #'from-string qualifiers))
(specializers (mapcar #'from-string specializers))
(method (find-method generic-function qualifiers specializers)))
(remove-method generic-function method)
t))
(defslyfun generic-method-specs (generic-name)
"Compute ((QUALIFIERS SPECIALIZERS)...) for methods of GENERIC-NAME's gf.
QUALIFIERS and SPECIALIZERS are lists of strings."
(mapcar
(lambda (method)
(list (mapcar #'prin1-to-string (slynk-mop:method-qualifiers method))
(mapcar (lambda (specializer)
(if (typep specializer 'slynk-mop:eql-specializer)
(format nil "(eql ~A)"
(slynk-mop:eql-specializer-object specializer))
(prin1-to-string (class-name specializer))))
(slynk-mop:method-specializers method))))
(slynk-mop:generic-function-methods (read-as-function generic-name))))
(defslyfun unintern-symbol (name package)
(let ((pkg (guess-package package)))
(cond ((not pkg) (format nil "No such package: ~s" package))
(t
(multiple-value-bind (sym found) (parse-symbol name pkg)
(case found
((nil) (format nil "~s not in package ~s" name package))
(t
(unintern sym pkg)
(format nil "Uninterned symbol: ~s" sym))))))))
(defslyfun slynk-delete-package (package-name)
(let ((pkg (or (guess-package package-name)
(error "No such package: ~s" package-name))))
(delete-package pkg)
nil))
(defslyfun find-definition-for-thing (thing)
(find-source-location thing))
(defslyfun find-source-location-for-emacs (spec)
(find-source-location (value-spec-ref spec)))
(defun value-spec-ref (spec)
(destructure-case spec
((:string string package)
(with-buffer-syntax (package)
(eval (read-from-string string))))
((:inspector part)
(inspector-nth-part part))
((:sly-db frame var)
(frame-var-value frame var))))
(defvar *find-definitions-right-trim* ",:.>")
(defvar *find-definitions-left-trim* "#:<")
(defun find-definitions-find-symbol-or-package (name)
(flet ((do-find (name)
(multiple-value-bind (symbol found name)
(with-buffer-syntax ()
(parse-symbol name))
(cond (found
(return-from find-definitions-find-symbol-or-package
(values symbol found)))
((find-package name)
(return-from find-definitions-find-symbol-or-package
(values (make-symbol name) t)))))))
(do-find name)
(do-find (string-right-trim *find-definitions-right-trim* name))
(do-find (string-left-trim *find-definitions-left-trim* name))
(do-find (string-left-trim *find-definitions-left-trim*
(string-right-trim
*find-definitions-right-trim* name)))
(when (and (eql (search "(setf " name :test #'char-equal) 0)
(char= (char name (1- (length name))) #\)))
(multiple-value-bind (symbol found)
(with-buffer-syntax ()
(parse-symbol (subseq name (length "(setf ")
(1- (length name)))))
(when found
(values `(setf ,symbol) t))))))
(defslyfun find-definitions-for-emacs (name)
"Return a list ((DSPEC LOCATION) ...) of definitions for NAME.
DSPEC is a string and LOCATION a source location. NAME is a string."
(multiple-value-bind (symbol found)
(find-definitions-find-symbol-or-package name)
(when found
(mapcar #'xref>elisp (find-definitions symbol)))))
(defgeneric xref-doit (type thing)
(:method (type thing)
(declare (ignore type thing))
:not-implemented))
(macrolet ((define-xref-action (xref-type handler)
`(defmethod xref-doit ((type (eql ,xref-type)) thing)
(declare (ignorable type))
(funcall ,handler thing))))
(define-xref-action :calls #'who-calls)
(define-xref-action :calls-who #'calls-who)
(define-xref-action :references #'who-references)
(define-xref-action :binds #'who-binds)
(define-xref-action :sets #'who-sets)
(define-xref-action :macroexpands #'who-macroexpands)
(define-xref-action :specializes #'who-specializes)
(define-xref-action :callers #'list-callers)
(define-xref-action :callees #'list-callees))
(defslyfun xref (type name)
(multiple-value-bind (sexp error) (ignore-errors (from-string name))
(unless error
(let ((xrefs (xref-doit type sexp)))
(if (eq xrefs :not-implemented)
:not-implemented
(mapcar #'xref>elisp xrefs))))))
(defslyfun xrefs (types name)
(loop for type in types
for xrefs = (xref type name)
when (and (not (eq :not-implemented xrefs))
(not (null xrefs)))
collect (cons type xrefs)))
(defun xref>elisp (xref)
(destructuring-bind (name loc) xref
(list (to-string name) loc)))
(defstruct (lcons (:constructor %lcons (car %cdr))
(:predicate lcons?))
car
(%cdr nil :type (or null lcons function))
(forced? nil))
(defmacro lcons (car cdr)
`(%lcons ,car (lambda () ,cdr)))
(defmacro lcons* (car cdr &rest more)
(cond ((null more) `(lcons ,car ,cdr))
(t `(lcons ,car (lcons* ,cdr ,@more)))))
(defun lcons-cdr (lcons)
(let ((cdr (lcons-%cdr lcons)))
(cond ((lcons-forced? lcons) cdr)
(t
(let ((value (funcall cdr)))
(setf (lcons-forced? lcons) t
(lcons-%cdr lcons) value))))))
(defun llist-range (llist start end)
(llist-take (llist-skip llist start) (- end start)))
(defun llist-skip (lcons index)
(do ((i 0 (1+ i))
(l lcons (lcons-cdr l)))
((or (= i index) (null l))
l)))
(defun llist-take (lcons count)
(let ((result '()))
(do ((i 0 (1+ i))
(l lcons (lcons-cdr l)))
((or (= i count)
(null l)))
(push (lcons-car l) result))
(nreverse result)))
(defun iline (label value)
`(:line ,label ,value))
(defvar-unbound *current-inspector*
"Current inspector, bound by EVAL-FOR-INSPECTOR, maybe to nil.")
(defvar-unbound *target-inspector*
"Target inspector, bound by EVAL-FOR-INSPECTOR, maybe to nil.")
(defun current-inspector ()
(or (and (boundp '*current-inspector*)
*current-inspector*)
(find-inspector "default")
(make-instance 'inspector :name "default")))
(defun target-inspector ()
(or (and (boundp '*target-inspector*)
*target-inspector*)
(current-inspector)))
(defvar *inspector-printer-bindings*
'((*print-lines* . 1)
(*print-right-margin* . 75)
(*print-pretty* . t)
(*print-readably* . nil)))
(defvar *inspector-verbose-printer-bindings*
'((*print-escape* . t)
(*print-circle* . t)
(*print-array* . nil)))
(defclass inspector ()
((verbose-p :initform nil :accessor inspector-verbose-p)
(history :initform (make-array 10 :adjustable t :fill-pointer 0) :accessor inspector-%history)
(name :initarg :name :initform (error "Name this INSPECTOR!") :accessor inspector-name)))
(defmethod print-object ((i inspector) s)
(print-unreadable-object (i s :type t)
(format s "~a/~a" (inspector-name i) (length (inspector-%history i)))))
(defmethod initialize-instance :after ((i inspector) &key name)
(assert (not (find-inspector name)) nil "Already have an inspector named ~a" name)
(push i (connection-inspectors *emacs-connection*)))
(defun find-inspector (name)
(find name (connection-inspectors *emacs-connection*)
:key #'inspector-name :test #'string=))
(defstruct inspector-state)
(defstruct (istate (:conc-name istate.) (:include inspector-state))
object
(parts (make-array 10 :adjustable t :fill-pointer 0))
(actions (make-array 10 :adjustable t :fill-pointer 0))
metadata
content
serial)
(defun ensure-istate-metadata (o indicator default)
(with-struct (istate. object metadata) (current-istate)
(assert (eq object o))
(let ((data (getf metadata indicator default)))
(setf (getf metadata indicator) data)
data)))
(defun current-istate (&optional (inspector (current-inspector)))
(let* ((history (inspector-%history inspector)))
(and (plusp (length history))
(aref history (1- (length history))))))
(defun reset-inspector (&optional (inspector (current-inspector)))
#+sbcl
(loop with hist = (inspector-%history inspector)
for i from 0 below (array-dimension hist 0)
do (setf (aref hist i) nil))
(setf (inspector-%history inspector)
(make-array 10 :adjustable t :fill-pointer 0)))
(defslyfun init-inspector (string)
(with-buffer-syntax ()
(with-retry-restart (:msg "Retry SLY inspection request.")
(inspect-object (eval (read-from-string string))))))
(defun inspect-object (o)
(let* ((inspector (target-inspector))
(history (inspector-%history inspector))
(istate (make-istate :object o)))
(vector-push-extend istate history)
(let ((*current-inspector* inspector))
(setf (istate.content istate)
(emacs-inspect o)))
(vector-push-extend :break-history history)
(decf (fill-pointer history))
(istate>elisp istate)))
(defun istate>elisp (istate)
(list :title (prepare-title istate)
:id (assign-index (istate.object istate) (istate.parts istate))
:content (prepare-range istate 0 500)
))
(defun prepare-title (istate)
(if (inspector-verbose-p (current-inspector))
(with-bindings *inspector-verbose-printer-bindings*
(to-string (istate.object istate)))
(with-string-stream (stream :length 200
:bindings *inspector-printer-bindings*)
(print-unreadable-object
((istate.object istate) stream :type t :identity t)))))
(defun prepare-range (istate start end)
(let* ((range (content-range (istate.content istate) start end))
(ps (loop for part in range append (prepare-part part istate))))
(list ps
(if (< (length ps) (- end start))
(+ start (length ps))
(+ end 1000))
start end)))
(defun prepare-part (part istate)
(let ((newline '#.(string #\newline)))
(etypecase part
(string (list part))
(cons (destructure-case part
((:newline) (list newline))
((:value obj &optional str)
(list (value-part obj str (istate.parts istate))))
((:label &rest strs)
(list (list :label (apply #'cat (mapcar #'string strs)))))
((:action label lambda &key (refreshp t))
(list (action-part label lambda refreshp
(istate.actions istate))))
((:line label value)
(list (princ-to-string label) ": "
(value-part value nil (istate.parts istate))
newline)))))))
(defun value-part (object string parts)
(list :value
(or string (print-part-to-string object))
(assign-index object parts)))
(defun action-part (label lambda refreshp actions)
(list :action label (assign-index (list lambda refreshp) actions)))
(defun assign-index (object vector)
(let ((index (fill-pointer vector)))
(vector-push-extend object vector)
index))
(defun print-part-to-string (value)
(let* ((*print-readably* nil)
(string (slynk-pprint-to-line value))
(pos (position value
(inspector-%history (current-inspector))
:key #'istate.object)))
(if pos
(format nil "@~D=~A" pos string)
string)))
(defun content-range (list start end)
(typecase list
(list (let ((len (length list)))
(subseq list start (min len end))))
(lcons (llist-range list start end))))
(defslyfun inspector-nth-part (index)
"Return the current inspector's INDEXth part.
The second value indicates if that part exists at all."
(let* ((parts (istate.parts (current-istate)))
(foundp (< index (length parts))))
(values (and foundp (aref parts index))
foundp)))
(defslyfun inspector-nth-part-or-lose (index)
"Return the current inspector's INDEXth part.
The second value indicates if that part exists at all."
(multiple-value-bind (part foundp)
(inspector-nth-part index)
(if foundp part (error "No part with index ~a" index))))
(defslyfun inspect-nth-part (index)
(with-buffer-syntax ()
(inspect-object (inspector-nth-part index))))
(defslyfun inspector-range (from to)
(prepare-range (current-istate) from to))
(defslyfun inspector-call-nth-action (index &rest args)
(destructuring-bind (fun refreshp) (aref (istate.actions (current-istate)) index)
(apply fun args)
(if refreshp
(inspector-reinspect)
nil)))
(defslyfun inspector-pop ()
"Inspect the previous object.
Return nil if there's no previous object."
(with-buffer-syntax ()
(let* ((history (inspector-%history (current-inspector))))
(when (> (length history) 1)
(decf (fill-pointer history))
(istate>elisp (current-istate))))))
(defslyfun inspector-next ()
"Inspect the next element in the history of inspected objects.."
(with-buffer-syntax ()
(let* ((history (inspector-%history (current-inspector))))
(when (and (< (fill-pointer history)
(array-dimension history 0))
(istate-p (aref history (fill-pointer history))))
(incf (fill-pointer history))
(istate>elisp (current-istate))))))
(defslyfun inspector-reinspect ()
(let ((istate (current-istate)))
(setf (istate.content istate)
(emacs-inspect (istate.object istate)))
(istate>elisp istate)))
(defslyfun inspector-toggle-verbose ()
"Toggle verbosity of inspected object."
(setf (inspector-verbose-p (current-inspector))
(not (inspector-verbose-p (current-inspector))))
(istate>elisp (current-istate)))
(defslyfun inspector-eval (string)
(let* ((obj (istate.object (current-istate)))
(context (eval-context obj))
(form (with-buffer-syntax ((cdr (assoc '*package* context)))
(read-from-string string)))
(ignorable (remove-if #'boundp (mapcar #'car context))))
(to-string (eval `(let ((* ',obj) (- ',form)
. ,(loop for (var . val) in context
unless (constantp var) collect
`(,var ',val)))
(declare (ignorable . ,ignorable))
,form)))))
(defslyfun inspector-history ()
(slynk-pprint-to-line (inspector-%history (current-inspector))))
(defslyfun quit-inspector ()
(reset-inspector)
nil)
(defslyfun describe-inspectee ()
"Describe the currently inspected object."
(with-buffer-syntax ()
(describe-to-string (istate.object (current-istate)))))
(defslyfun describe-inspector-part (index)
"Describe part INDEX of the currently inspected object."
(with-buffer-syntax ()
(describe-to-string (inspector-nth-part index))))
(defslyfun pprint-inspector-part (index)
"Pretty-print part INDEX of the currently inspected object."
(with-buffer-syntax ()
(slynk-pprint (inspector-nth-part index))))
(defslyfun inspect-in-frame (string index)
(with-buffer-syntax ()
(with-retry-restart (:msg "Retry SLY inspection request.")
(reset-inspector)
(inspect-object (eval-in-frame (from-string string) index)))))
(defslyfun inspect-current-condition ()
(with-buffer-syntax ()
(reset-inspector)
(inspect-object *slynk-debugger-condition*)))
(defslyfun inspect-frame-var (frame var)
(with-buffer-syntax ()
(reset-inspector)
(inspect-object (frame-var-value frame var))))
(defslyfun pprint-frame-var (frame var)
(with-buffer-syntax ()
(slynk-pprint (frame-var-value frame var))))
(defslyfun describe-frame-var (frame var)
(with-buffer-syntax ()
(describe-to-string (frame-var-value frame var))))
(defslyfun eval-for-inspector (current
target
slave-slyfun &rest args)
"Call SLAVE-SLYFUN with ARGS in CURRENT inspector, open in TARGET."
(let ((*current-inspector* (and current
(or (find-inspector current)
(make-instance 'inspector :name current))))
(*target-inspector* (and target
(or (find-inspector target)
(make-instance 'inspector :name target)))))
(apply slave-slyfun args)))
(defmethod emacs-inspect ((o cons))
(if (listp (cdr o))
(inspect-list o)
(inspect-cons o)))
(defun inspect-cons (cons)
(label-value-line*
('car (car cons))
('cdr (cdr cons))))
(defun inspect-list (list)
(multiple-value-bind (length tail) (safe-length list)
(flet ((frob (title list)
(list* title '(:newline) (inspect-list-aux list))))
(cond ((not length)
(frob "A circular list:"
(cons (car list)
(ldiff (cdr list) list))))
((not tail)
(frob "A proper list:" list))
(t
(frob "An improper list:" list))))))
(defun inspect-list-aux (list)
(loop for i from 0 for rest on list while (consp rest) append
(if (listp (cdr rest))
(label-value-line i (car rest))
(label-value-line* (i (car rest)) (:tail (cdr rest))))))
(defun safe-length (list)
"Similar to `list-length', but avoid errors on improper lists.
Return two values: the length of the list and the last cdr.
Return NIL if LIST is circular."
(do ((n 0 (+ n 2)) (fast list (cddr fast)) (slow list (cdr slow))) (nil)
(cond ((null fast) (return (values n nil)))
((not (consp fast)) (return (values n fast)))
((null (cdr fast)) (return (values (1+ n) (cdr fast))))
((and (eq fast slow) (> n 0)) (return nil))
((not (consp (cdr fast))) (return (values (1+ n) (cdr fast)))))))
(defun hash-table-to-alist (ht)
(let ((result '()))
(maphash (lambda (key value)
(setq result (acons key value result)))
ht)
result))
(defmethod emacs-inspect ((ht hash-table))
(append
(label-value-line*
("Count" (hash-table-count ht))
("Size" (hash-table-size ht))
("Test" (hash-table-test ht))
("Rehash size" (hash-table-rehash-size ht))
("Rehash threshold" (hash-table-rehash-threshold ht)))
(let ((weakness (hash-table-weakness ht)))
(when weakness
(label-value-line "Weakness:" weakness)))
(unless (zerop (hash-table-count ht))
`((:action "[clear hashtable]"
,(lambda () (clrhash ht))) (:newline)
"Contents: " (:newline)))
(let ((content (hash-table-to-alist ht)))
(cond ((every (lambda (x) (typep (first x) '(or string symbol))) content)
(setf content (sort content 'string< :key #'first)))
((every (lambda (x) (typep (first x) 'real)) content)
(setf content (sort content '< :key #'first))))
(loop for (key . value) in content appending
`((:value ,key) " = " (:value ,value)
" " (:action "[remove entry]"
,(let ((key key))
(lambda () (remhash key ht))))
(:newline))))))
(defmethod emacs-inspect ((array array))
(lcons*
(iline "Dimensions" (array-dimensions array))
(iline "Element type" (array-element-type array))
(iline "Total size" (array-total-size array))
(iline "Adjustable" (adjustable-array-p array))
(iline "Fill pointer" (if (array-has-fill-pointer-p array)
(fill-pointer array)))
"Contents:" '(:newline)
(labels ((k (i max)
(cond ((= i max) '())
(t (lcons (iline i (row-major-aref array i))
(k (1+ i) max))))))
(k 0 (array-total-size array)))))
(defmethod emacs-inspect :around (object)
(declare (ignore object))
(with-bindings (if (inspector-verbose-p (current-inspector))
*inspector-verbose-printer-bindings*
*inspector-printer-bindings*)
(call-next-method)))
(defmethod emacs-inspect ((char character))
(append
(label-value-line*
("Char code" (char-code char))
("Lower cased" (char-downcase char))
("Upper cased" (char-upcase char)))
(if (get-macro-character char)
`("In the current readtable ("
(:value ,*readtable*) ") it is a macro character: "
(:value ,(get-macro-character char))))))
(defvar *thread-list* ()
"List of threads displayed in Emacs. We don't care a about
synchronization issues (yet). There can only be one thread listing at
a time.")
(defslyfun list-threads ()
"Return a list (LABELS (ID NAME STATUS ATTRS ...) ...).
LABELS is a list of attribute names and the remaining lists are the
corresponding attribute values per thread.
Example:
((:id :name :status :priority)
(6 \"slynk-indentation-cache-thread\" \"Semaphore timed wait\" 0)
(5 \"reader-thread\" \"Active\" 0)
(4 \"control-thread\" \"Semaphore timed wait\" 0)
(2 \"Slynk Sentinel\" \"Semaphore timed wait\" 0)
(1 \"listener\" \"Active\" 0)
(0 \"Initial\" \"Sleep\" 0))"
(setq *thread-list* (all-threads))
(when (and *emacs-connection*
(use-threads-p)
(equalp (thread-name (current-thread)) "slynk-worker"))
(setf *thread-list* (delete (current-thread) *thread-list*)))
(let* ((plist (thread-attributes (car *thread-list*)))
(labels (loop for (key) on plist by #'cddr
collect key)))
`((:id :name :status ,@labels)
,@(loop for thread in *thread-list*
for name = (thread-name thread)
for attributes = (thread-attributes thread)
collect (list* (thread-id thread)
(string name)
(thread-status thread)
(loop for label in labels
collect (getf attributes label)))))))
(defslyfun quit-thread-browser ()
(setq *thread-list* nil))
(defun nth-thread (index)
(nth index *thread-list*))
(defslyfun debug-nth-thread (index)
(let ((connection *emacs-connection*))
(queue-thread-interrupt
(nth-thread index)
(lambda ()
(with-connection (connection)
(simple-break))))))
(defslyfun kill-nth-thread (index)
(kill-thread (nth-thread index)))
(defslyfun start-slynk-server-in-thread (index port-file-name)
"Interrupt the INDEXth thread and make it start a slynk server.
The server port is written to PORT-FILE-NAME."
(interrupt-thread (nth-thread index)
(lambda ()
(start-server port-file-name :style nil))))
(defun mop-helper (class-name fn)
(let ((class (find-class class-name nil)))
(if class
(mapcar (lambda (x) (to-string (class-name x)))
(funcall fn class)))))
(defslyfun mop (type symbol-name)
"Return info about classes using mop.
When type is:
:subclasses - return the list of subclasses of class.
:superclasses - return the list of superclasses of class."
(let ((symbol (parse-symbol symbol-name *buffer-package*)))
(ecase type
(:subclasses
(mop-helper symbol #'slynk-mop:class-direct-subclasses))
(:superclasses
(mop-helper symbol #'slynk-mop:class-direct-superclasses)))))
(defun sync-features-to-emacs ()
"Update Emacs if any relevant Lisp state has changed."
(unless (eq *sly-features* *features*)
(setq *sly-features* *features*)
(send-to-emacs (list :new-features (features-for-emacs)))))
(defun features-for-emacs ()
"Return `*sly-features*' in a format suitable to send it to Emacs."
*sly-features*)
(add-hook *pre-reply-hook* 'sync-features-to-emacs)
(defvar *configure-emacs-indentation* t
"When true, automatically send indentation information to Emacs
after each command.")
(defslyfun update-indentation-information ()
(send-to-indentation-cache `(:update-indentation-information))
nil)
(defun sync-indentation-to-emacs ()
"Send any indentation updates to Emacs via CONNECTION."
(when *configure-emacs-indentation*
(send-to-indentation-cache `(:sync-indentation ,*buffer-package*))))
(defun send-to-indentation-cache (request)
(let ((c *emacs-connection*))
(etypecase c
(singlethreaded-connection
(handle-indentation-cache-request c request))
(multithreaded-connection
(without-sly-interrupts
(send (mconn.indentation-cache-thread c) request)))
(null t))))
(defun indentation-cache-loop (connection)
(with-connection (connection)
(loop
(restart-case
(handle-indentation-cache-request connection (receive))
(abort ()
:report "Return to the indentation cache request handling loop.")))))
(defun handle-indentation-cache-request (connection request)
(destructure-case request
((:sync-indentation package)
(when (package-name package)
(let ((fullp (need-full-indentation-update-p connection)))
(perform-indentation-update connection fullp package))))
((:update-indentation-information)
(perform-indentation-update connection t nil))))
(defun need-full-indentation-update-p (connection)
"Return true if the whole indentation cache should be updated.
This is a heuristic to avoid scanning all symbols all the time:
instead, we only do a full scan if the set of packages has changed."
(set-difference (list-all-packages)
(connection-indentation-cache-packages connection)))
(defun perform-indentation-update (connection force package)
"Update the indentation cache in CONNECTION and update Emacs.
If FORCE is true then start again without considering the old cache."
(let ((cache (connection-indentation-cache connection)))
(when force (clrhash cache))
(let ((delta (update-indentation/delta-for-emacs cache force package)))
(setf (connection-indentation-cache-packages connection)
(list-all-packages))
(unless (null delta)
(setf (connection-indentation-cache connection) cache)
(send-to-emacs (list :indentation-update delta))))))
(defun update-indentation/delta-for-emacs (cache force package)
"Update the cache and return the changes in a (SYMBOL INDENT PACKAGES) list.
If FORCE is true then check all symbols, otherwise only check symbols
belonging to PACKAGE."
(let ((alist '()))
(flet ((consider (symbol)
(let ((indent (symbol-indentation symbol)))
(when indent
(unless (equal (gethash symbol cache) indent)
(setf (gethash symbol cache) indent)
(let ((pkgs (mapcar #'package-name
(symbol-packages symbol)))
(name (string-downcase symbol)))
(push (list name indent pkgs) alist)))))))
(cond (force
(do-all-symbols (symbol)
(consider symbol)))
((package-name package) (do-symbols (symbol package)
(when (eq (symbol-package symbol) package)
(consider symbol)))))
alist)))
(defun package-names (package)
"Return the name and all nicknames of PACKAGE in a fresh list."
(cons (package-name package) (copy-list (package-nicknames package))))
(defun symbol-packages (symbol)
"Return the packages where SYMBOL can be found."
(let ((string (string symbol)))
(loop for p in (list-all-packages)
when (eq symbol (find-symbol string p))
collect p)))
(defun cl-symbol-p (symbol)
"Is SYMBOL a symbol in the COMMON-LISP package?"
(eq (symbol-package symbol) cl-package))
(defun known-to-emacs-p (symbol)
"Return true if Emacs has special rules for indenting SYMBOL."
(cl-symbol-p symbol))
(defun symbol-indentation (symbol)
"Return a form describing the indentation of SYMBOL.
The form is to be used as the `sly-common-lisp-indent-function' property
in Emacs."
(if (and (macro-function symbol)
(not (known-to-emacs-p symbol)))
(let ((arglist (arglist symbol)))
(etypecase arglist
((member :not-available)
nil)
(list
(macro-indentation arglist))))
nil))
(defun macro-indentation (arglist)
(if (well-formed-list-p arglist)
(position '&body (remove '&optional (clean-arglist arglist)))
nil))
(defun clean-arglist (arglist)
"Remove &whole, &enviroment, and &aux elements from ARGLIST."
(cond ((null arglist) '())
((member (car arglist) '(&whole &environment))
(clean-arglist (cddr arglist)))
((eq (car arglist) '&aux)
'())
(t (cons (car arglist) (clean-arglist (cdr arglist))))))
(defun well-formed-list-p (list)
"Is LIST a proper list terminated by NIL?"
(typecase list
(null t)
(cons (well-formed-list-p (cdr list)))
(t nil)))
(defun print-indentation-lossage (&optional (stream *standard-output*))
"Return the list of symbols whose indentation styles collide incompatibly.
Collisions are caused because package information is ignored."
(let ((table (make-hash-table :test 'equal)))
(flet ((name (s) (string-downcase (symbol-name s))))
(do-all-symbols (s)
(setf (gethash (name s) table)
(cons s (symbol-indentation s))))
(let ((collisions '()))
(do-all-symbols (s)
(let* ((entry (gethash (name s) table))
(owner (car entry))
(indent (cdr entry)))
(unless (or (eq s owner)
(equal (symbol-indentation s) indent)
(and (not (fboundp s))
(null (macro-function s))))
(pushnew owner collisions)
(pushnew s collisions))))
(if (null collisions)
(format stream "~&No worries!~%")
(format stream "~&Symbols with collisions:~%~{ ~S~%~}"
collisions))))))
#-clasp
(add-hook *pre-reply-hook* 'sync-indentation-to-emacs)
(defslyfun io-speed-test (&optional (n 1000) (m 1))
(let* ((s *standard-output*)
(*trace-output* (make-broadcast-stream s *log-output*)))
(time (progn
(dotimes (i n)
(format s "~D abcdefghijklm~%" i)
(when (zerop (mod n m))
(finish-output s)))
(finish-output s)
(when *emacs-connection*
(eval-in-emacs '(message "done.")))))
(terpri *trace-output*)
(finish-output *trace-output*)
nil))
(defslyfun flow-control-test (n delay)
(let ((stream (make-output-stream
(let ((conn *emacs-connection*))
(lambda (string)
(declare (ignore string))
(with-connection (conn)
(send-to-emacs `(:test-delay ,delay))))))))
(dotimes (i n)
(print i stream)
(force-output stream)
(background-message "flow-control-test: ~d" i))))
(defpackage :slynk-api (:use))
(eval-when (:compile-toplevel :load-toplevel :execute)
(let ((api '(#:*emacs-connection*
#:*m-x-sly-from-emacs*
#:default-connection
#:channel
#:channel-id
#:channel-thread-id
#:close-channel
#:define-channel-method
#:find-channel
#:send-to-remote-channel
#:*channel*
#:listener
#:with-listener-bindings
#:saving-listener-bindings
#:flush-listener-streams
#:default-listener
#:close-listener
#:add-hook
#:*connection-closed-hook*
#:*after-init-hook*
#:*new-connection-hook*
#:*pre-reply-hook*
#:*after-toggle-trace-hook*
#:*eval-for-emacs-wrappers*
#:*debugger-extra-options*
#:*buffer-readtable*
#:defslyfun
#:destructure-case
#:log-event
#:process-requests
#:use-threads-p
#:wait-for-event
#:with-bindings
#:with-connection
#:with-top-level-restart
#:with-sly-interrupts
#:with-buffer-syntax
#:with-retry-restart
#:*loaded-user-init-file*
#:load-user-init-file
#:make-thread-bindings-aware-lambda
#:package-string-for-prompt
#:*slynk-wire-protocol-version*
#:*slynk-require-hook*
#:present-for-emacs
#:cl-package
#:+keyword-package+
#:guess-package
#:guess-buffer-package
#:*exclude-symbol-functions*
#:*buffer-package*
#:*slynk-io-package*
#:parse-package
#:tokenize-symbol
#:untokenize-symbol
#:symbol-external-p
#:unparse-name
#:excluded-from-searches-p
#:slynk-pprint
#:slynk-pprint-values
#:slynk-pprint-to-line
#:background-message
#:map-if)))
(loop for sym in api
for slynk-api-sym = (intern (string sym) :slynk-api)
for slynk-sym = (intern (string sym) :slynk)
do (unintern slynk-api-sym :slynk-api)
(import slynk-sym :slynk-api)
(export slynk-sym :slynk-api))))
(defvar *loaded-user-init-file* nil
"User init file actually loaded from user's home, if any.")
(defun load-user-init-file ()
"Load the user init file, return NIL if it does not exist."
(find-if (lambda (homedir-file)
(load (merge-pathnames (user-homedir-pathname)
homedir-file)
:if-does-not-exist nil))
(list (make-pathname :name ".slynk" :type "lisp")
(make-pathname :name ".slynkrc"))))
(defun init ()
(unless (member :slynk *features*)
(pushnew :slynk *features*))
(setq *loaded-user-init-file* (load-user-init-file))
(run-hook *after-init-hook*))