(in-package swank)
(defpackage swank-repl
(:use cl swank/backend)
(:export *send-repl-results-function*)
(:import-from
swank
*default-worker-thread-bindings*
*loopback-interface*
add-hook
*connection-closed-hook*
eval-region
with-buffer-syntax
connection
connection.socket-io
connection.repl-results
connection.user-input
connection.user-output
connection.user-io
connection.trace-output
connection.dedicated-output
connection.env
multithreaded-connection
mconn.active-threads
mconn.repl-thread
mconn.auto-flush-thread
use-threads-p
*emacs-connection*
default-connection
with-connection
send-to-emacs
*communication-style*
handle-requests
wait-for-event
make-tag
thread-for-evaluation
socket-quest
authenticate-client
encode-message
auto-flush-loop
clear-user-input
current-thread-id
cat
with-struct*
with-retry-restart
with-bindings
package-string-for-prompt
find-external-format-or-lose
defslimefun
*use-dedicated-output-stream*
*dedicated-output-stream-port*
*globally-redirect-io*))
(in-package swank-repl)
(defvar *use-dedicated-output-stream* nil
"When T swank will attempt to create a second connection to Emacs
which is used just to send output.")
(defvar *dedicated-output-stream-port* 0
"Which port we should use for the dedicated output stream.")
(defvar *dedicated-output-stream-buffering*
(if (eq *communication-style* :spawn) t nil)
"The buffering scheme that should be used for the output stream.
Valid values are nil, t, :line")
(defvar *globally-redirect-io* :started-from-emacs
"When T globally redirect all standard streams to Emacs.
When :STARTED-FROM-EMACS redirect when launched by M-x slime")
(defun globally-redirect-io-p ()
(case *globally-redirect-io*
((t) t)
(:started-from-emacs swank-loader:*started-from-emacs*)))
(defun open-streams (connection properties)
"Return the 5 streams for IO redirection:
DEDICATED-OUTPUT INPUT OUTPUT IO REPL-RESULTS"
(let* ((input-fn
(lambda ()
(with-connection (connection)
(with-simple-restart (abort-read
"Abort reading input from Emacs.")
(read-user-input-from-emacs)))))
(dedicated-output (if *use-dedicated-output-stream*
(open-dedicated-output-stream
connection
(getf properties :coding-system))))
(in (make-input-stream input-fn))
(out (or dedicated-output
(make-output-stream (make-output-function connection))))
(io (make-two-way-stream in out))
(repl-results (swank:make-output-stream-for-target connection
:repl-result)))
(typecase connection
(multithreaded-connection
(setf (mconn.auto-flush-thread connection)
(make-auto-flush-thread out))))
(values dedicated-output in out io repl-results)))
(defun make-output-function (connection)
"Create function to send user output to Emacs."
(lambda (string)
(with-connection (connection)
(send-to-emacs `(:write-string ,string nil ,(current-thread-id)))
(wait-for-event `(:write-done)))))
(defun open-dedicated-output-stream (connection coding-system)
"Open a dedicated output connection to the Emacs on SOCKET-IO.
Return an output stream suitable for writing program output.
This is an optimized way for Lisp to deliver output to Emacs."
(let ((socket (socket-quest *dedicated-output-stream-port* nil))
(ef (find-external-format-or-lose coding-system)))
(unwind-protect
(let ((port (local-port socket)))
(encode-message `(:open-dedicated-output-stream ,port
,coding-system)
(connection.socket-io connection))
(let ((dedicated (accept-connection
socket
:external-format ef
:buffering *dedicated-output-stream-buffering*
:timeout 30)))
(authenticate-client dedicated)
(close-socket socket)
(setf socket nil)
dedicated))
(when socket
(close-socket socket)))))
(defmethod thread-for-evaluation ((connection multithreaded-connection)
(id (eql :find-existing)))
(or (car (mconn.active-threads connection))
(find-repl-thread connection)))
(defmethod thread-for-evaluation ((connection multithreaded-connection)
(id (eql :repl-thread)))
(find-repl-thread connection))
(defun find-repl-thread (connection)
(cond ((not (use-threads-p))
(current-thread))
(t
(let ((thread (mconn.repl-thread connection)))
(cond ((not thread) nil)
((thread-alive-p thread) thread)
(t
(setf (mconn.repl-thread connection)
(spawn-repl-thread connection "new-repl-thread"))))))))
(defun spawn-repl-thread (connection name)
(spawn (lambda ()
(with-bindings *default-worker-thread-bindings*
(repl-loop connection)))
:name name))
(defun repl-loop (connection)
(handle-requests connection))
(defslimefun create-repl (target &key coding-system)
(assert (eq target nil))
(let ((conn *emacs-connection*))
(initialize-streams-for-connection conn `(:coding-system ,coding-system))
(with-struct* (connection. @ conn)
(setf (@ env)
`((*standard-input* . ,(@ user-input))
,@(unless (globally-redirect-io-p)
`((*standard-output* . ,(@ user-output))
(*trace-output* . ,(or (@ trace-output) (@ user-output)))
(*error-output* . ,(@ user-output))
(*debug-io* . ,(@ user-io))
(*query-io* . ,(@ user-io))
(*terminal-io* . ,(@ user-io))))))
(maybe-redirect-global-io conn)
(add-hook *connection-closed-hook* 'update-redirection-after-close)
(typecase conn
(multithreaded-connection
(setf (mconn.repl-thread conn)
(spawn-repl-thread conn "repl-thread"))))
(list (package-name *package*)
(package-string-for-prompt *package*)))))
(defun initialize-streams-for-connection (connection properties)
(multiple-value-bind (dedicated in out io repl-results)
(open-streams connection properties)
(setf (connection.dedicated-output connection) dedicated
(connection.user-io connection) io
(connection.user-output connection) out
(connection.user-input connection) in
(connection.repl-results connection) repl-results)
connection))
(defun read-user-input-from-emacs ()
(let ((tag (make-tag)))
(force-output)
(send-to-emacs `(:read-string ,(current-thread-id) ,tag))
(let ((ok nil))
(unwind-protect
(prog1 (caddr (wait-for-event `(:emacs-return-string ,tag value)))
(swank/gray::reset-stream-line-column (connection.user-output *emacs-connection*))
(setq ok t))
(unless ok
(send-to-emacs `(:read-aborted ,(current-thread-id) ,tag)))))))
(defvar *listener-eval-function* 'repl-eval)
(defvar *listener-saved-value* nil)
(defslimefun listener-save-value (slimefun &rest args)
"Apply SLIMEFUN to ARGS and save the value.
The saved value should be visible to all threads and retrieved via
LISTENER-GET-VALUE."
(setq *listener-saved-value* (apply slimefun args))
t)
(defslimefun listener-get-value ()
"Get the last value saved by LISTENER-SAVE-VALUE.
The value should be produced as if it were requested through
LISTENER-EVAL directly, so that spacial variables *, etc are set."
(listener-eval (let ((*package* (find-package :keyword)))
(write-to-string '*listener-saved-value*))))
(defslimefun listener-eval (string &key (window-width nil window-width-p))
(swank/gray::reset-stream-line-column (connection.user-output *emacs-connection*))
(if window-width-p
(let ((*print-right-margin* window-width))
(funcall *listener-eval-function* string))
(funcall *listener-eval-function* string)))
(defslimefun clear-repl-variables ()
(let ((variables '(*** ** * /// // / +++ ++ +)))
(loop for variable in variables
do (setf (symbol-value variable) nil))))
(defvar *send-repl-results-function* 'send-repl-results-to-emacs)
(defun repl-eval (string)
(clear-user-input)
(with-buffer-syntax ()
(with-retry-restart (:msg "Retry SLIME REPL evaluation request.")
(track-package
(lambda ()
(multiple-value-bind (values last-form) (eval-region string)
(setq *** ** ** * * (car values)
/// // // / / values
+++ ++ ++ + + last-form)
(funcall *send-repl-results-function* values))))))
nil)
(defun track-package (fun)
(let ((p *package*))
(unwind-protect (funcall fun)
(unless (eq *package* p)
(send-to-emacs (list :new-package (package-name *package*)
(package-string-for-prompt *package*)))))))
(defun send-repl-results-to-emacs (values)
(finish-output)
(if (null values)
(send-to-emacs `(:write-string "; No value" :repl-result))
(dolist (v values)
(send-to-emacs `(:write-string ,(cat (prin1-to-string v) #\newline)
:repl-result)))))
(defslimefun redirect-trace-output (target)
(setf (connection.trace-output *emacs-connection*)
(swank:make-output-stream-for-target *emacs-connection* target))
nil)
(defvar *saved-global-streams* '()
"A plist to save and restore redirected stream objects.
E.g. the value for '*standard-output* holds the stream object
for *standard-output* before we install our redirection.")
(defun setup-stream-indirection (stream-var &optional stream)
"Setup redirection scaffolding for a global stream variable.
Supposing (for example) STREAM-VAR is *STANDARD-INPUT*, this macro:
1. Saves the value of *STANDARD-INPUT* in `*SAVED-GLOBAL-STREAMS*'.
2. Creates *CURRENT-STANDARD-INPUT*, initially with the same value as
*STANDARD-INPUT*.
3. Assigns *STANDARD-INPUT* to a synonym stream pointing to
*CURRENT-STANDARD-INPUT*.
This has the effect of making *CURRENT-STANDARD-INPUT* contain the
effective global value for *STANDARD-INPUT*. This way we can assign
the effective global value even when *STANDARD-INPUT* is shadowed by a
dynamic binding."
(let ((current-stream-var (prefixed-var '#:current stream-var))
(stream (or stream (symbol-value stream-var))))
(setf (getf *saved-global-streams* stream-var) stream)
(proclaim `(special ,current-stream-var))
(set current-stream-var stream)
(let ((stream (make-synonym-stream current-stream-var)))
(set stream-var stream)
(set-default-initial-binding stream-var `(quote ,stream)))))
(defun prefixed-var (prefix variable-symbol)
"(PREFIXED-VAR \"FOO\" '*BAR*) => SWANK::*FOO-BAR*"
(let ((basename (subseq (symbol-name variable-symbol) 1)))
(intern (format nil "*~A-~A" (string prefix) basename) :swank)))
(defvar *standard-output-streams*
'(*standard-output* *error-output* *trace-output*)
"The symbols naming standard output streams.")
(defvar *standard-input-streams*
'(*standard-input*)
"The symbols naming standard input streams.")
(defvar *standard-io-streams*
'(*debug-io* *query-io* *terminal-io*)
"The symbols naming standard io streams.")
(defun init-global-stream-redirection ()
(when (globally-redirect-io-p)
(cond (*saved-global-streams*
(warn "Streams already redirected."))
(t
(mapc #'setup-stream-indirection
(append *standard-output-streams*
*standard-input-streams*
*standard-io-streams*))))))
(defun globally-redirect-io-to-connection (connection)
"Set the standard I/O streams to redirect to CONNECTION.
Assigns *CURRENT-<STREAM>* for all standard streams."
(dolist (o *standard-output-streams*)
(set (prefixed-var '#:current o)
(connection.user-output connection)))
#+NIL
(dolist (i *standard-input-streams*)
(set (prefixed-var '#:current i)
(connection.user-input connection)))
(dolist (io *standard-io-streams*)
(set (prefixed-var '#:current io)
(connection.user-io connection))))
(defun revert-global-io-redirection ()
"Set *CURRENT-<STREAM>* to *REAL-<STREAM>* for all standard streams."
(dolist (stream-var (append *standard-output-streams*
*standard-input-streams*
*standard-io-streams*))
(set (prefixed-var '#:current stream-var)
(getf *saved-global-streams* stream-var))))
(defvar *global-stdio-connection* nil
"The connection to which standard I/O streams are globally redirected.
NIL if streams are not globally redirected.")
(defun maybe-redirect-global-io (connection)
"Consider globally redirecting to CONNECTION."
(when (and (globally-redirect-io-p) (null *global-stdio-connection*)
(connection.user-io connection))
(unless *saved-global-streams*
(init-global-stream-redirection))
(setq *global-stdio-connection* connection)
(globally-redirect-io-to-connection connection)))
(defun update-redirection-after-close (closed-connection)
"Update redirection after a connection closes."
(check-type closed-connection connection)
(when (eq *global-stdio-connection* closed-connection)
(if (and (default-connection) (globally-redirect-io-p))
(globally-redirect-io-to-connection (default-connection))
(progn (revert-global-io-redirection)
(setq *global-stdio-connection* nil)))))
(provide :swank-repl)