|
;;; nrepl-client.el --- Client for Clojure nREPL -*- lexical-binding: t -*-
|
|
|
|
;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
|
|
;; Copyright © 2013-2019 Bozhidar Batsov, Artur Malabarba and CIDER contributors
|
|
;;
|
|
;; Author: Tim King <kingtim@gmail.com>
|
|
;; Phil Hagelberg <technomancy@gmail.com>
|
|
;; Bozhidar Batsov <bozhidar@batsov.com>
|
|
;; Artur Malabarba <bruce.connor.am@gmail.com>
|
|
;; Hugo Duncan <hugo@hugoduncan.org>
|
|
;; Steve Purcell <steve@sanityinc.com>
|
|
;; Reid McKenzie <me@arrdem.com>
|
|
;;
|
|
;; This program is free software: you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
;;
|
|
;; This program is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
;;
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
;;
|
|
;; This file is not part of GNU Emacs.
|
|
;;
|
|
;;; Commentary:
|
|
;;
|
|
;; Provides an Emacs Lisp client to connect to Clojure nREPL servers.
|
|
;;
|
|
;; A connection is an abstract idea of the communication between Emacs (client)
|
|
;; and nREPL server. On the Emacs side connections are represented by two
|
|
;; running processes. The two processes are the server process and client
|
|
;; process (the connection to the server). Each of these is represented by its
|
|
;; own process buffer, filter and sentinel.
|
|
;;
|
|
;; The nREPL communication process can be broadly represented as follows:
|
|
;;
|
|
;; 1) The server process is started as an Emacs subprocess (usually by
|
|
;; `cider-jack-in', which in turn fires up leiningen or boot). Note that
|
|
;; if a connection was established using `cider-connect' there won't be
|
|
;; a server process.
|
|
;;
|
|
;; 2) The server's process filter (`nrepl-server-filter') detects the
|
|
;; connection port from the first plain text response from the server and
|
|
;; starts a communication process (socket connection) as another Emacs
|
|
;; subprocess. This is the nREPL client process (`nrepl-client-filter').
|
|
;; All requests and responses handling happens through this client
|
|
;; connection.
|
|
;;
|
|
;; 3) Requests are sent by `nrepl-send-request' and
|
|
;; `nrepl-send-sync-request'. A request is simply a list containing a
|
|
;; requested operation name and the parameters required by the
|
|
;; operation. Each request has an associated callback that is called once
|
|
;; the response for the request has arrived. Besides the above functions
|
|
;; there are specialized request senders for each type of common
|
|
;; operations. Examples are `nrepl-request:eval', `nrepl-request:clone',
|
|
;; `nrepl-sync-request:describe'.
|
|
;;
|
|
;; 4) Responses from the server are decoded in `nrepl-client-filter' and are
|
|
;; physically represented by alists whose structure depends on the type of
|
|
;; the response. After having been decoded, the data from the response is
|
|
;; passed over to the callback that was registered by the original
|
|
;; request.
|
|
;;
|
|
;; Please see the comments in dedicated sections of this file for more detailed
|
|
;; description.
|
|
|
|
;;; Code:
|
|
(require 'seq)
|
|
(require 'subr-x)
|
|
(require 'cider-compat)
|
|
(require 'cl-lib)
|
|
(require 'nrepl-dict)
|
|
(require 'queue)
|
|
(require 'tramp)
|
|
|
|
|
|
;;; Custom
|
|
|
|
(defgroup nrepl nil
|
|
"Interaction with the Clojure nREPL Server."
|
|
:prefix "nrepl-"
|
|
:group 'applications)
|
|
|
|
;; (defcustom nrepl-buffer-name-separator " "
|
|
;; "Used in constructing the REPL buffer name.
|
|
;; The `nrepl-buffer-name-separator' separates cider-repl from the project name."
|
|
;; :type '(string)
|
|
;; :group 'nrepl)
|
|
(make-obsolete-variable 'nrepl-buffer-name-separator 'cider-session-name-template "0.18")
|
|
|
|
;; (defcustom nrepl-buffer-name-show-port nil
|
|
;; "Show the connection port in the nrepl REPL buffer name, if set to t."
|
|
;; :type 'boolean
|
|
;; :group 'nrepl)
|
|
(make-obsolete-variable 'nrepl-buffer-name-show-port 'cider-session-name-template "0.18")
|
|
|
|
(defcustom nrepl-connected-hook nil
|
|
"List of functions to call when connecting to the nREPL server."
|
|
:type 'hook
|
|
:group 'nrepl)
|
|
|
|
(defcustom nrepl-disconnected-hook nil
|
|
"List of functions to call when disconnected from the nREPL server."
|
|
:type 'hook
|
|
:group 'nrepl)
|
|
|
|
(defcustom nrepl-file-loaded-hook nil
|
|
"List of functions to call when a load file has completed."
|
|
:type 'hook
|
|
:group 'nrepl)
|
|
|
|
(defcustom nrepl-force-ssh-for-remote-hosts nil
|
|
"If non-nil, do not attempt a direct connection for remote hosts."
|
|
:type 'boolean
|
|
:group 'nrepl)
|
|
|
|
(defcustom nrepl-use-ssh-fallback-for-remote-hosts nil
|
|
"If non-nil, attempt to connect via ssh to remote hosts when unable to connect directly."
|
|
:type 'boolean
|
|
:group 'nrepl)
|
|
|
|
(defcustom nrepl-sync-request-timeout 10
|
|
"The number of seconds to wait for a sync response.
|
|
Setting this to nil disables the timeout functionality."
|
|
:type 'integer
|
|
:group 'nrepl)
|
|
|
|
(defcustom nrepl-hide-special-buffers nil
|
|
"Control the display of some special buffers in buffer switching commands.
|
|
When true some special buffers like the server buffer will be hidden."
|
|
:type 'boolean
|
|
:group 'nrepl)
|
|
|
|
|
|
;;; Buffer Local Declarations
|
|
|
|
;; These variables are used to track the state of nREPL connections
|
|
(defvar-local nrepl-connection-buffer nil)
|
|
(defvar-local nrepl-server-buffer nil)
|
|
(defvar-local nrepl-messages-buffer nil)
|
|
(defvar-local nrepl-endpoint nil)
|
|
(defvar-local nrepl-project-dir nil)
|
|
(defvar-local nrepl-is-server nil)
|
|
(defvar-local nrepl-server-command nil)
|
|
(defvar-local nrepl-tunnel-buffer nil)
|
|
|
|
(defvar-local nrepl-session nil
|
|
"Current nREPL session id.")
|
|
|
|
(defvar-local nrepl-tooling-session nil
|
|
"Current nREPL tooling session id.
|
|
To be used for tooling calls (i.e. completion, eldoc, etc)")
|
|
|
|
(defvar-local nrepl-request-counter 0
|
|
"Continuation serial number counter.")
|
|
|
|
(defvar-local nrepl-pending-requests nil)
|
|
|
|
(defvar-local nrepl-completed-requests nil)
|
|
|
|
(defvar-local nrepl-last-sync-response nil
|
|
"Result of the last sync request.")
|
|
|
|
(defvar-local nrepl-last-sync-request-timestamp nil
|
|
"The time when the last sync request was initiated.")
|
|
|
|
(defvar-local nrepl-ops nil
|
|
"Available nREPL server ops (from describe).")
|
|
|
|
(defvar-local nrepl-versions nil
|
|
"Version information received from the describe op.")
|
|
|
|
(defvar-local nrepl-aux nil
|
|
"Auxiliary information received from the describe op.")
|
|
|
|
|
|
;;; nREPL Buffer Names
|
|
|
|
(defconst nrepl-message-buffer-name-template "*nrepl-messages %s(%r:%S)*")
|
|
(defconst nrepl-error-buffer-name "*nrepl-error*")
|
|
(defconst nrepl-repl-buffer-name-template "*cider-repl %s(%r:%S)*")
|
|
(defconst nrepl-server-buffer-name-template "*nrepl-server %s*")
|
|
(defconst nrepl-tunnel-buffer-name-template "*nrepl-tunnel %s*")
|
|
|
|
(defun nrepl-make-buffer-name (template params &optional dup-ok)
|
|
"Generate a buffer name using TEMPLATE and PARAMS.
|
|
TEMPLATE and PARAMS are as in `cider-format-connection-params'. If
|
|
optional DUP-OK is non-nil, the returned buffer is not \"uniquified\" by a
|
|
call to `generate-new-buffer-name'."
|
|
(let ((name (cider-format-connection-params template params)))
|
|
(if dup-ok
|
|
name
|
|
(generate-new-buffer-name name))))
|
|
|
|
(defun nrepl--make-hidden-name (buffer-name)
|
|
"Apply a prefix to BUFFER-NAME that will hide the buffer."
|
|
(concat (if nrepl-hide-special-buffers " " "") buffer-name))
|
|
|
|
(defun nrepl-repl-buffer-name (params &optional dup-ok)
|
|
"Return the name of the repl buffer.
|
|
PARAMS and DUP-OK are as in `nrepl-make-buffer-name'."
|
|
(nrepl-make-buffer-name nrepl-repl-buffer-name-template params dup-ok))
|
|
|
|
(defun nrepl-server-buffer-name (params)
|
|
"Return the name of the server buffer.
|
|
PARAMS is as in `nrepl-make-buffer-name'."
|
|
(nrepl--make-hidden-name
|
|
(nrepl-make-buffer-name nrepl-server-buffer-name-template params)))
|
|
|
|
(defun nrepl-tunnel-buffer-name (params)
|
|
"Return the name of the tunnel buffer.
|
|
PARAMS is as in `nrepl-make-buffer-name'."
|
|
(nrepl--make-hidden-name
|
|
(nrepl-make-buffer-name nrepl-tunnel-buffer-name-template params)))
|
|
|
|
(defun nrepl-messages-buffer-name (params)
|
|
"Return the name for the message buffer given connection PARAMS."
|
|
(nrepl-make-buffer-name nrepl-message-buffer-name-template params))
|
|
|
|
|
|
;;; Utilities
|
|
(defun nrepl-op-supported-p (op connection)
|
|
"Return t iff the given operation OP is supported by the nREPL CONNECTION."
|
|
(when (buffer-live-p connection)
|
|
(with-current-buffer connection
|
|
(and nrepl-ops (nrepl-dict-get nrepl-ops op)))))
|
|
|
|
(defun nrepl-aux-info (key connection)
|
|
"Return KEY's aux info, as returned via the :describe op for CONNECTION."
|
|
(with-current-buffer connection
|
|
(and nrepl-aux (nrepl-dict-get nrepl-aux key))))
|
|
|
|
(defun nrepl-local-host-p (host)
|
|
"Return t if HOST is local."
|
|
(string-match-p tramp-local-host-regexp host))
|
|
|
|
(defun nrepl-extract-port (dir)
|
|
"Read port from .nrepl-port, nrepl-port or target/repl-port files in directory DIR."
|
|
(or (nrepl--port-from-file (expand-file-name "repl-port" dir))
|
|
(nrepl--port-from-file (expand-file-name ".nrepl-port" dir))
|
|
(nrepl--port-from-file (expand-file-name "target/repl-port" dir))
|
|
(nrepl--port-from-file (expand-file-name ".shadow-cljs/nrepl.port" dir))))
|
|
|
|
(defun nrepl--port-from-file (file)
|
|
"Attempts to read port from a file named by FILE."
|
|
(when (file-exists-p file)
|
|
(with-temp-buffer
|
|
(insert-file-contents file)
|
|
(buffer-string))))
|
|
|
|
|
|
;;; Bencode
|
|
|
|
(cl-defstruct (nrepl-response-queue
|
|
(:include queue)
|
|
(:constructor nil)
|
|
(:constructor nrepl-response-queue (&optional stub)))
|
|
stub)
|
|
|
|
(put 'nrepl-response-queue 'function-documentation
|
|
"Create queue object used by nREPL to store decoded server responses.
|
|
The STUB slot stores a stack of nested, incompletely parsed objects.")
|
|
|
|
(defun nrepl--bdecode-list (&optional stack)
|
|
"Decode a bencode list or dict starting at point.
|
|
STACK is as in `nrepl--bdecode-1'."
|
|
;; skip leading l or d
|
|
(forward-char 1)
|
|
(let* ((istack (nrepl--bdecode-1 stack))
|
|
(pos0 (point))
|
|
(info (car istack)))
|
|
(while (null info)
|
|
(setq istack (nrepl--bdecode-1 (cdr istack))
|
|
pos0 (point)
|
|
info (car istack)))
|
|
(cond ((eq info :e)
|
|
(cons nil (cdr istack)))
|
|
((eq info :stub)
|
|
(goto-char pos0)
|
|
istack)
|
|
(t istack))))
|
|
|
|
(defun nrepl--bdecode-1 (&optional stack)
|
|
"Decode one elementary bencode object starting at point.
|
|
Bencoded object is either list, dict, integer or string. See
|
|
http://en.wikipedia.org/wiki/Bencode#Encoding_algorithm for the encoding
|
|
rules.
|
|
|
|
STACK is a list of so far decoded components of the current message. Car
|
|
of STACK is the innermost incompletely decoded object. The algorithm pops
|
|
this list when inner object was completely decoded or grows it by one when
|
|
new list or dict was encountered.
|
|
|
|
The returned value is of the form (INFO . STACK) where INFO is
|
|
:stub, nil, :end or :eob and STACK is either an incomplete parsing state as
|
|
above (INFO is :stub, nil or :eob) or a list of one component representing
|
|
the completely decoded message (INFO is :end). INFO is nil when an
|
|
elementary non-root object was successfully decoded. INFO is :end when this
|
|
object is a root list or dict."
|
|
(cond
|
|
;; list
|
|
((eq (char-after) ?l)
|
|
(nrepl--bdecode-list (cons () stack)))
|
|
;; dict
|
|
((eq (char-after) ?d)
|
|
(nrepl--bdecode-list (cons '(dict) stack)))
|
|
;; end of a list or a dict
|
|
((eq (char-after) ?e)
|
|
(forward-char 1)
|
|
(cons (if (cdr stack) :e :end)
|
|
(nrepl--push (nrepl--nreverse (car stack))
|
|
(cdr stack))))
|
|
;; string
|
|
((looking-at "\\([0-9]+\\):")
|
|
(let ((pos0 (point))
|
|
(beg (goto-char (match-end 0)))
|
|
(end (byte-to-position (+ (position-bytes (point))
|
|
(string-to-number (match-string 1))))))
|
|
(if (null end)
|
|
(progn (goto-char pos0)
|
|
(cons :stub stack))
|
|
(goto-char end)
|
|
;; normalise any platform-specific newlines
|
|
(let* ((original (buffer-substring-no-properties beg end))
|
|
(result (replace-regexp-in-string "\r\n\\|\n\r\\|\r" "\n" original)))
|
|
(cons nil (nrepl--push result stack))))))
|
|
;; integer
|
|
((looking-at "i\\(-?[0-9]+\\)e")
|
|
(goto-char (match-end 0))
|
|
(cons nil (nrepl--push (string-to-number (match-string 1))
|
|
stack)))
|
|
;; should happen in tests only as eobp is checked in nrepl-bdecode.
|
|
((eobp)
|
|
(cons :eob stack))
|
|
;; truncation in the middle of an integer or in 123: string prefix
|
|
((looking-at-p "[0-9i]")
|
|
(cons :stub stack))
|
|
;; else, throw a quiet error
|
|
(t
|
|
(message "Invalid bencode message detected. See the %s buffer for details."
|
|
nrepl-error-buffer-name)
|
|
(nrepl-log-error
|
|
(format "Decoder error at position %d (`%s'):"
|
|
(point) (buffer-substring (point) (min (+ (point) 10) (point-max)))))
|
|
(nrepl-log-error (buffer-string))
|
|
(ding)
|
|
;; Ensure loop break and clean queues' states in nrepl-bdecode:
|
|
(goto-char (point-max))
|
|
(cons :end nil))))
|
|
|
|
(defun nrepl--bdecode-message (&optional stack)
|
|
"Decode one full message starting at point.
|
|
STACK is as in `nrepl--bdecode-1'. Return a cons (INFO . STACK)."
|
|
(let* ((istack (nrepl--bdecode-1 stack))
|
|
(info (car istack))
|
|
(stack (cdr istack)))
|
|
(while (or (null info)
|
|
(eq info :e))
|
|
(setq istack (nrepl--bdecode-1 stack)
|
|
info (car istack)
|
|
stack (cdr istack)))
|
|
istack))
|
|
|
|
(defun nrepl-bdecode (string-q &optional response-q)
|
|
"Decode STRING-Q and place the results into RESPONSE-Q.
|
|
STRING-Q is either a queue of strings or a string. RESPONSE-Q is a queue of
|
|
server requests (nREPL dicts). STRING-Q and RESPONSE-Q are modified by side
|
|
effects.
|
|
|
|
Return a cons (STRING-Q . RESPONSE-Q) where STRING-Q is the original queue
|
|
containing the remainder of the input strings which could not be
|
|
decoded. RESPONSE-Q is the original queue with successfully decoded messages
|
|
enqueued and with slot STUB containing a nested stack of an incompletely
|
|
decoded message or nil if the strings were completely decoded."
|
|
(with-current-buffer (get-buffer-create " *nrepl-decoding*")
|
|
(fundamental-mode)
|
|
(erase-buffer)
|
|
(if (queue-p string-q)
|
|
(while (queue-head string-q)
|
|
(insert (queue-dequeue string-q)))
|
|
(insert string-q)
|
|
(setq string-q (queue-create)))
|
|
(goto-char 1)
|
|
(unless response-q
|
|
(setq response-q (nrepl-response-queue)))
|
|
(let ((istack (nrepl--bdecode-message
|
|
(nrepl-response-queue-stub response-q))))
|
|
(while (and (eq (car istack) :end)
|
|
(not (eobp)))
|
|
(queue-enqueue response-q (cadr istack))
|
|
(setq istack (nrepl--bdecode-message)))
|
|
(unless (eobp)
|
|
(queue-enqueue string-q (buffer-substring (point) (point-max))))
|
|
(if (not (eq (car istack) :end))
|
|
(setf (nrepl-response-queue-stub response-q) (cdr istack))
|
|
(queue-enqueue response-q (cadr istack))
|
|
(setf (nrepl-response-queue-stub response-q) nil))
|
|
(erase-buffer)
|
|
(cons string-q response-q))))
|
|
|
|
(defun nrepl-bencode (object)
|
|
"Encode OBJECT with bencode.
|
|
Integers, lists and nrepl-dicts are treated according to bencode
|
|
specification. Everything else is encoded as string."
|
|
(cond
|
|
((integerp object) (format "i%de" object))
|
|
((nrepl-dict-p object) (format "d%se" (mapconcat #'nrepl-bencode (cdr object) "")))
|
|
((listp object) (format "l%se" (mapconcat #'nrepl-bencode object "")))
|
|
(t (format "%s:%s" (string-bytes object) object))))
|
|
|
|
|
|
;;; Client: Process Filter
|
|
|
|
(defvar nrepl-response-handler-functions nil
|
|
"List of functions to call on each nREPL message.
|
|
Each of these functions should be a function with one argument, which will
|
|
be called by `nrepl-client-filter' on every response received. The current
|
|
buffer will be connection (REPL) buffer of the process. These functions
|
|
should take a single argument, a dict representing the message. See
|
|
`nrepl--dispatch-response' for an example.
|
|
|
|
These functions are called before the message's own callbacks, so that they
|
|
can affect the behaviour of the callbacks. Errors signaled by these
|
|
functions are demoted to messages, so that they don't prevent the
|
|
callbacks from running.")
|
|
|
|
(defun nrepl-client-filter (proc string)
|
|
"Decode message(s) from PROC contained in STRING and dispatch them."
|
|
(let ((string-q (process-get proc :string-q)))
|
|
(queue-enqueue string-q string)
|
|
;; Start decoding only if the last letter is 'e'
|
|
(when (eq ?e (aref string (1- (length string))))
|
|
(let ((response-q (process-get proc :response-q)))
|
|
(nrepl-bdecode string-q response-q)
|
|
(while (queue-head response-q)
|
|
(with-current-buffer (process-buffer proc)
|
|
(let ((response (queue-dequeue response-q)))
|
|
(with-demoted-errors "Error in one of the `nrepl-response-handler-functions': %s"
|
|
(run-hook-with-args 'nrepl-response-handler-functions response))
|
|
(nrepl--dispatch-response response))))))))
|
|
|
|
(defun nrepl--dispatch-response (response)
|
|
"Dispatch the RESPONSE to associated callback.
|
|
First we check the callbacks of pending requests. If no callback was found,
|
|
we check the completed requests, since responses could be received even for
|
|
older requests with \"done\" status."
|
|
(nrepl-dbind-response response (id)
|
|
(nrepl-log-message response 'response)
|
|
(let ((callback (or (gethash id nrepl-pending-requests)
|
|
(gethash id nrepl-completed-requests))))
|
|
(if callback
|
|
(funcall callback response)
|
|
(error "[nREPL] No response handler with id %s found" id)))))
|
|
|
|
(defun nrepl-client-sentinel (process message)
|
|
"Handle sentinel events from PROCESS.
|
|
Notify MESSAGE and if the process is closed run `nrepl-disconnected-hook'
|
|
and kill the process buffer."
|
|
(if (string-match "deleted\\b" message)
|
|
(message "[nREPL] Connection closed")
|
|
(message "[nREPL] Connection closed unexpectedly (%s)"
|
|
(substring message 0 -1)))
|
|
(when (equal (process-status process) 'closed)
|
|
(when-let* ((client-buffer (process-buffer process)))
|
|
(sesman-remove-object 'CIDER nil client-buffer
|
|
(not (process-get process :keep-server))
|
|
'no-error)
|
|
(nrepl--clear-client-sessions client-buffer)
|
|
(with-current-buffer client-buffer
|
|
(goto-char (point-max))
|
|
(insert-before-markers
|
|
(propertize
|
|
(format "\n*** Closed on %s ***\n" (current-time-string))
|
|
'face 'cider-repl-stderr-face))
|
|
(run-hooks 'nrepl-disconnected-hook)
|
|
(let ((server-buffer nrepl-server-buffer))
|
|
(when (and (buffer-live-p server-buffer)
|
|
(not (process-get process :keep-server)))
|
|
(setq nrepl-server-buffer nil)
|
|
(nrepl--maybe-kill-server-buffer server-buffer)))))))
|
|
|
|
|
|
;;; Network
|
|
|
|
(defun nrepl-connect (host port)
|
|
"Connect to the nREPL server identified by HOST and PORT.
|
|
For local hosts use a direct connection. For remote hosts, if
|
|
`nrepl-force-ssh-for-remote-hosts' is nil, attempt a direct connection
|
|
first. If `nrepl-force-ssh-for-remote-hosts' is non-nil or the direct
|
|
connection failed (and `nrepl-use-ssh-fallback-for-remote-hosts' is
|
|
non-nil), try to start a SSH tunneled connection. Return a plist of the
|
|
form (:proc PROC :host \"HOST\" :port PORT) that might contain additional
|
|
key-values depending on the connection type."
|
|
(let ((localp (if host
|
|
(nrepl-local-host-p host)
|
|
(not (file-remote-p default-directory)))))
|
|
(if localp
|
|
(nrepl--direct-connect (or host "localhost") port)
|
|
;; we're dealing with a remote host
|
|
(if (and host (not nrepl-force-ssh-for-remote-hosts))
|
|
(or (nrepl--direct-connect host port 'no-error)
|
|
;; direct connection failed
|
|
;; fallback to ssh tunneling if enabled
|
|
(and nrepl-use-ssh-fallback-for-remote-hosts
|
|
(message "[nREPL] Falling back to SSH tunneled connection ...")
|
|
(nrepl--ssh-tunnel-connect host port))
|
|
;; fallback is either not enabled or it failed as well
|
|
(if (and (null nrepl-use-ssh-fallback-for-remote-hosts)
|
|
(not localp))
|
|
(error "[nREPL] Direct connection to %s:%s failed; try setting `nrepl-use-ssh-fallback-for-remote-hosts' to t"
|
|
host port)
|
|
(error "[nREPL] Cannot connect to %s:%s" host port)))
|
|
;; `nrepl-force-ssh-for-remote-hosts' is non-nil
|
|
(nrepl--ssh-tunnel-connect host port)))))
|
|
|
|
(defun nrepl--direct-connect (host port &optional no-error)
|
|
"If HOST and PORT are given, try to `open-network-stream'.
|
|
If NO-ERROR is non-nil, show messages instead of throwing an error."
|
|
(if (not (and host port))
|
|
(unless no-error
|
|
(unless host
|
|
(error "[nREPL] Host not provided"))
|
|
(unless port
|
|
(error "[nREPL] Port not provided")))
|
|
(message "[nREPL] Establishing direct connection to %s:%s ..." host port)
|
|
(condition-case nil
|
|
(prog1 (list :proc (open-network-stream "nrepl-connection" nil host port)
|
|
:host host :port port)
|
|
(message "[nREPL] Direct connection to %s:%s established" host port))
|
|
(error (let ((msg (format "[nREPL] Direct connection to %s:%s failed" host port)))
|
|
(if no-error
|
|
(message msg)
|
|
(error msg))
|
|
nil)))))
|
|
|
|
(defun nrepl--ssh-tunnel-connect (host port)
|
|
"Connect to a remote machine identified by HOST and PORT through SSH tunnel."
|
|
(message "[nREPL] Establishing SSH tunneled connection to %s:%s ..." host port)
|
|
(let* ((remote-dir (if host (format "/ssh:%s:" host) default-directory))
|
|
(ssh (or (executable-find "ssh")
|
|
(error "[nREPL] Cannot locate 'ssh' executable")))
|
|
(cmd (nrepl--ssh-tunnel-command ssh remote-dir port))
|
|
(tunnel-buf (nrepl-tunnel-buffer-name
|
|
`((:host ,host) (:port ,port))))
|
|
(tunnel (start-process-shell-command "nrepl-tunnel" tunnel-buf cmd)))
|
|
(process-put tunnel :waiting-for-port t)
|
|
(set-process-filter tunnel (nrepl--ssh-tunnel-filter port))
|
|
(while (and (process-live-p tunnel)
|
|
(process-get tunnel :waiting-for-port))
|
|
(accept-process-output nil 0.005))
|
|
(if (not (process-live-p tunnel))
|
|
(error "[nREPL] SSH port forwarding failed. Check the '%s' buffer" tunnel-buf)
|
|
(message "[nREPL] SSH port forwarding established to localhost:%s" port)
|
|
(let ((endpoint (nrepl--direct-connect "localhost" port)))
|
|
(thread-first endpoint
|
|
(plist-put :tunnel tunnel)
|
|
(plist-put :remote-host host))))))
|
|
|
|
(defun nrepl--ssh-tunnel-command (ssh dir port)
|
|
"Command string to open SSH tunnel to the host associated with DIR's PORT."
|
|
(with-parsed-tramp-file-name dir v
|
|
;; this abuses the -v option for ssh to get output when the port
|
|
;; forwarding is set up, which is used to synchronise on, so that
|
|
;; the port forwarding is up when we try to connect.
|
|
(format-spec
|
|
"%s -v -N -L %p:localhost:%p %u'%h'"
|
|
`((?s . ,ssh)
|
|
(?p . ,port)
|
|
(?h . ,v-host)
|
|
(?u . ,(if v-user (format "-l '%s' " v-user) ""))))))
|
|
|
|
(autoload 'comint-watch-for-password-prompt "comint" "(autoload).")
|
|
|
|
(defun nrepl--ssh-tunnel-filter (port)
|
|
"Return a process filter that waits for PORT to appear in process output."
|
|
(let ((port-string (format "LOCALHOST:%s" port)))
|
|
(lambda (proc string)
|
|
(when (string-match-p port-string string)
|
|
(process-put proc :waiting-for-port nil))
|
|
(when (and (process-live-p proc)
|
|
(buffer-live-p (process-buffer proc)))
|
|
(with-current-buffer (process-buffer proc)
|
|
(let ((moving (= (point) (process-mark proc))))
|
|
(save-excursion
|
|
(goto-char (process-mark proc))
|
|
(insert string)
|
|
(set-marker (process-mark proc) (point))
|
|
(comint-watch-for-password-prompt string))
|
|
(if moving (goto-char (process-mark proc)))))))))
|
|
|
|
|
|
;;; Client: Process Handling
|
|
|
|
(defun nrepl--kill-process (proc)
|
|
"Kill PROC using the appropriate, os specific way.
|
|
Implement a workaround to clean up an orphaned JVM process left around
|
|
after exiting the REPL on some windows machines."
|
|
(if (memq system-type '(cygwin windows-nt))
|
|
(interrupt-process proc)
|
|
(kill-process proc)))
|
|
|
|
(defun nrepl-kill-server-buffer (server-buf)
|
|
"Kill SERVER-BUF and its process."
|
|
(when (buffer-live-p server-buf)
|
|
(let ((proc (get-buffer-process server-buf)))
|
|
(when (process-live-p proc)
|
|
(set-process-query-on-exit-flag proc nil)
|
|
(nrepl--kill-process proc))
|
|
(kill-buffer server-buf))))
|
|
|
|
(defun nrepl--maybe-kill-server-buffer (server-buf)
|
|
"Kill SERVER-BUF and its process.
|
|
Do not kill the server if there is a REPL connected to that server."
|
|
(when (buffer-live-p server-buf)
|
|
(with-current-buffer server-buf
|
|
;; Don't kill if there is at least one REPL connected to it.
|
|
(when (not (seq-find (lambda (b)
|
|
(eq (buffer-local-value 'nrepl-server-buffer b)
|
|
server-buf))
|
|
(buffer-list)))
|
|
(nrepl-kill-server-buffer server-buf)))))
|
|
|
|
(defun nrepl-start-client-process (&optional host port server-proc buffer-builder)
|
|
"Create new client process identified by HOST and PORT.
|
|
In remote buffers, HOST and PORT are taken from the current tramp
|
|
connection. SERVER-PROC must be a running nREPL server process within
|
|
Emacs. BUFFER-BUILDER is a function of one argument (endpoint returned by
|
|
`nrepl-connect') which returns a client buffer. Return the newly created
|
|
client process."
|
|
(let* ((endpoint (nrepl-connect host port))
|
|
(client-proc (plist-get endpoint :proc))
|
|
(builder (or buffer-builder (error "`buffer-builder' must be provided")))
|
|
(client-buf (funcall builder endpoint)))
|
|
|
|
(set-process-buffer client-proc client-buf)
|
|
|
|
(set-process-filter client-proc 'nrepl-client-filter)
|
|
(set-process-sentinel client-proc 'nrepl-client-sentinel)
|
|
(set-process-coding-system client-proc 'utf-8-unix 'utf-8-unix)
|
|
|
|
(process-put client-proc :string-q (queue-create))
|
|
(process-put client-proc :response-q (nrepl-response-queue))
|
|
|
|
(with-current-buffer client-buf
|
|
(when-let* ((server-buf (and server-proc (process-buffer server-proc))))
|
|
(setq nrepl-project-dir (buffer-local-value 'nrepl-project-dir server-buf)
|
|
nrepl-server-buffer server-buf))
|
|
(setq nrepl-endpoint endpoint
|
|
nrepl-tunnel-buffer (when-let* ((tunnel (plist-get endpoint :tunnel)))
|
|
(process-buffer tunnel))
|
|
nrepl-pending-requests (make-hash-table :test 'equal)
|
|
nrepl-completed-requests (make-hash-table :test 'equal)))
|
|
|
|
(with-current-buffer client-buf
|
|
(nrepl--init-client-sessions client-proc)
|
|
(nrepl--init-capabilities client-buf)
|
|
(run-hooks 'nrepl-connected-hook))
|
|
|
|
client-proc))
|
|
|
|
(defun nrepl--init-client-sessions (client)
|
|
"Initialize CLIENT connection nREPL sessions.
|
|
We create two client nREPL sessions per connection - a main session and a
|
|
tooling session. The main session is general purpose and is used for pretty
|
|
much every request that needs a session. The tooling session is used only
|
|
for functionality that's implemented in terms of the \"eval\" op, so that
|
|
eval requests for functionality like pretty-printing won't clobber the
|
|
values of *1, *2, etc."
|
|
(let* ((client-conn (process-buffer client))
|
|
(response-main (nrepl-sync-request:clone client-conn))
|
|
(response-tooling (nrepl-sync-request:clone client-conn t))) ; t for tooling
|
|
(nrepl-dbind-response response-main (new-session err)
|
|
(if new-session
|
|
(with-current-buffer client-conn
|
|
(setq nrepl-session new-session))
|
|
(error "Could not create new session (%s)" err)))
|
|
(nrepl-dbind-response response-tooling (new-session err)
|
|
(if new-session
|
|
(with-current-buffer client-conn
|
|
(setq nrepl-tooling-session new-session))
|
|
(error "Could not create new tooling session (%s)" err)))))
|
|
|
|
(defun nrepl--init-capabilities (conn-buffer)
|
|
"Store locally in CONN-BUFFER the capabilities of nREPL server."
|
|
(let ((description (nrepl-sync-request:describe conn-buffer)))
|
|
(nrepl-dbind-response description (ops versions aux)
|
|
(with-current-buffer conn-buffer
|
|
(setq nrepl-ops ops)
|
|
(setq nrepl-versions versions)
|
|
(setq nrepl-aux aux)))))
|
|
|
|
(defun nrepl--clear-client-sessions (conn-buffer)
|
|
"Clear information about nREPL sessions in CONN-BUFFER.
|
|
CONN-BUFFER refers to a (presumably) dead connection, which we can eventually reuse."
|
|
(with-current-buffer conn-buffer
|
|
(setq nrepl-session nil)
|
|
(setq nrepl-tooling-session nil)))
|
|
|
|
|
|
;;; Client: Response Handling
|
|
;; After being decoded, responses (aka, messages from the server) are dispatched
|
|
;; to handlers. Handlers are constructed with `nrepl-make-response-handler'.
|
|
|
|
(defvar nrepl-err-handler nil
|
|
"Evaluation error handler.")
|
|
|
|
(defun nrepl--mark-id-completed (id)
|
|
"Move ID from `nrepl-pending-requests' to `nrepl-completed-requests'.
|
|
It is safe to call this function multiple times on the same ID."
|
|
;; FIXME: This should go away eventually when we get rid of
|
|
;; pending-request hash table
|
|
(when-let* ((handler (gethash id nrepl-pending-requests)))
|
|
(puthash id handler nrepl-completed-requests)
|
|
(remhash id nrepl-pending-requests)))
|
|
|
|
(declare-function cider-repl--emit-interactive-output "cider-repl")
|
|
(defun nrepl-notify (msg type)
|
|
"Handle \"notification\" server request.
|
|
MSG is a string to be displayed. TYPE is the type of the message. All
|
|
notifications are currently displayed with `message' function and emitted
|
|
to the REPL."
|
|
(let* ((face (pcase type
|
|
((or "message" `nil) 'font-lock-builtin-face)
|
|
("warning" 'warning)
|
|
("error" 'error)))
|
|
(msg (if face
|
|
(propertize msg 'face face)
|
|
(format "%s: %s" (upcase type) msg))))
|
|
(cider-repl--emit-interactive-output msg (or face 'font-lock-builtin-face))
|
|
(message msg)))
|
|
|
|
(defvar cider-buffer-ns)
|
|
(defvar cider-special-mode-truncate-lines)
|
|
(declare-function cider-need-input "cider-client")
|
|
(declare-function cider-set-buffer-ns "cider-mode")
|
|
|
|
(defun nrepl-make-response-handler (buffer value-handler stdout-handler
|
|
stderr-handler done-handler
|
|
&optional eval-error-handler
|
|
content-type-handler
|
|
truncated-handler)
|
|
"Make a response handler for connection BUFFER.
|
|
A handler is a function that takes one argument - response received from
|
|
the server process. The response is an alist that contains at least 'id'
|
|
and 'session' keys. Other standard response keys are 'value', 'out', 'err',
|
|
and 'status'.
|
|
|
|
The presence of a particular key determines the type of the response. For
|
|
example, if 'value' key is present, the response is of type 'value', if
|
|
'out' key is present the response is 'stdout' etc.
|
|
|
|
Depending on the type, the handler dispatches the appropriate value to one
|
|
of the supplied handlers: VALUE-HANDLER, STDOUT-HANDLER, STDERR-HANDLER,
|
|
DONE-HANDLER, EVAL-ERROR-HANDLER, CONTENT-TYPE-HANDLER, and
|
|
TRUNCATED-HANDLER.
|
|
|
|
Handlers are functions of the buffer and the value they handle, except for
|
|
the optional CONTENT-TYPE-HANDLER which should be a function of the buffer,
|
|
content, the content-type to be handled as a list `(type attrs)'.
|
|
|
|
If the optional EVAL-ERROR-HANDLER is nil, the default `nrepl-err-handler'
|
|
is used. If any of the other supplied handlers are nil nothing happens for
|
|
the corresponding type of response."
|
|
(lambda (response)
|
|
(nrepl-dbind-response response (content-type content-transfer-encoding body
|
|
value ns out err status id)
|
|
(when (buffer-live-p buffer)
|
|
(with-current-buffer buffer
|
|
(when (and ns (not (derived-mode-p 'clojure-mode)))
|
|
(cider-set-buffer-ns ns))))
|
|
(cond ((and content-type content-type-handler)
|
|
(funcall content-type-handler buffer
|
|
(if (string= content-transfer-encoding "base64")
|
|
(base64-decode-string body)
|
|
body)
|
|
content-type))
|
|
(value
|
|
(when value-handler
|
|
(funcall value-handler buffer value)))
|
|
(out
|
|
(when stdout-handler
|
|
(funcall stdout-handler buffer out)))
|
|
(err
|
|
(when stderr-handler
|
|
(funcall stderr-handler buffer err)))
|
|
(status
|
|
(when (and truncated-handler (member "nrepl.middleware.print/truncated" status))
|
|
(let ((warning (format "\n... output truncated to %sB ..."
|
|
(file-size-human-readable cider-print-quota))))
|
|
(funcall truncated-handler buffer warning)))
|
|
(when (member "notification" status)
|
|
(nrepl-dbind-response response (msg type)
|
|
(nrepl-notify msg type)))
|
|
(when (member "interrupted" status)
|
|
(message "Evaluation interrupted."))
|
|
(when (member "eval-error" status)
|
|
(funcall (or eval-error-handler nrepl-err-handler)))
|
|
(when (member "namespace-not-found" status)
|
|
(message "Namespace `%s' not found." ns))
|
|
(when (member "need-input" status)
|
|
(cider-need-input buffer))
|
|
(when (member "done" status)
|
|
(nrepl--mark-id-completed id)
|
|
(when done-handler
|
|
(funcall done-handler buffer))))))))
|
|
|
|
|
|
;;; Client: Request Core API
|
|
|
|
;; Requests are messages from an nREPL client (like CIDER) to an nREPL server.
|
|
;; Requests can be asynchronous (sent with `nrepl-send-request') or
|
|
;; synchronous (send with `nrepl-send-sync-request'). The request is a pair list
|
|
;; of operation name and operation parameters. The core operations are described
|
|
;; at https://github.com/nrepl/nrepl/blob/master/doc/ops.md. CIDER adds
|
|
;; many more operations through nREPL middleware. See
|
|
;; https://github.com/clojure-emacs/cider-nrepl#supplied-nrepl-middleware for
|
|
;; the up-to-date list.
|
|
|
|
(defun nrepl-next-request-id (connection)
|
|
"Return the next request id for CONNECTION."
|
|
(with-current-buffer connection
|
|
(number-to-string (cl-incf nrepl-request-counter))))
|
|
|
|
(defun nrepl-send-request (request callback connection &optional tooling)
|
|
"Send REQUEST and register response handler CALLBACK using CONNECTION.
|
|
REQUEST is a pair list of the form (\"op\" \"operation\" \"par1-name\"
|
|
\"par1\" ... ). See the code of `nrepl-request:clone',
|
|
`nrepl-request:stdin', etc. This expects that the REQUEST does not have a
|
|
session already in it. This code will add it as appropriate to prevent
|
|
connection/session drift.
|
|
Return the ID of the sent message.
|
|
Optional argument TOOLING Set to t if desiring the tooling session rather than the standard session."
|
|
(with-current-buffer connection
|
|
(when-let* ((session (if tooling nrepl-tooling-session nrepl-session)))
|
|
(setq request (append request `("session" ,session))))
|
|
(let* ((id (nrepl-next-request-id connection))
|
|
(request (cons 'dict (lax-plist-put request "id" id)))
|
|
(message (nrepl-bencode request)))
|
|
(nrepl-log-message request 'request)
|
|
(puthash id callback nrepl-pending-requests)
|
|
(process-send-string nil message)
|
|
id)))
|
|
|
|
(defvar nrepl-ongoing-sync-request nil
|
|
"Dynamically bound to t while a sync request is ongoing.")
|
|
|
|
(declare-function cider-repl-emit-interactive-stderr "cider-repl")
|
|
(declare-function cider--render-stacktrace-causes "cider-eval")
|
|
|
|
(defun nrepl-send-sync-request (request connection &optional abort-on-input tooling)
|
|
"Send REQUEST to the nREPL server synchronously using CONNECTION.
|
|
Hold till final \"done\" message has arrived and join all response messages
|
|
of the same \"op\" that came along.
|
|
If ABORT-ON-INPUT is non-nil, the function will return nil at the first
|
|
sign of user input, so as not to hang the interface.
|
|
If TOOLING, use the tooling session rather than the standard session."
|
|
(let* ((time0 (current-time))
|
|
(response (cons 'dict nil))
|
|
(nrepl-ongoing-sync-request t)
|
|
status)
|
|
(nrepl-send-request request
|
|
(lambda (resp) (nrepl--merge response resp))
|
|
connection
|
|
tooling)
|
|
(while (and (not (member "done" status))
|
|
(not (and abort-on-input
|
|
(input-pending-p))))
|
|
(setq status (nrepl-dict-get response "status"))
|
|
;; If we get a need-input message then the repl probably isn't going
|
|
;; anywhere, and we'll just timeout. So we forward it to the user.
|
|
(if (member "need-input" status)
|
|
(progn (cider-need-input (current-buffer))
|
|
;; If the used took a few seconds to respond, we might
|
|
;; unnecessarily timeout, so let's reset the timer.
|
|
(setq time0 (current-time)))
|
|
;; break out in case we don't receive a response for a while
|
|
(when (and nrepl-sync-request-timeout
|
|
(> (cadr (time-subtract (current-time) time0))
|
|
nrepl-sync-request-timeout))
|
|
(error "Sync nREPL request timed out %s" request)))
|
|
;; Clean up the response, otherwise we might repeatedly ask for input.
|
|
(nrepl-dict-put response "status" (remove "need-input" status))
|
|
(accept-process-output nil 0.01))
|
|
;; If we couldn't finish, return nil.
|
|
(when (member "done" status)
|
|
(nrepl-dbind-response response (ex err eval-error pp-stacktrace id)
|
|
(when (and ex err)
|
|
(cond (eval-error (funcall nrepl-err-handler))
|
|
(pp-stacktrace (cider--render-stacktrace-causes
|
|
pp-stacktrace (remove "done" status))))) ;; send the error type
|
|
(when id
|
|
(with-current-buffer connection
|
|
(nrepl--mark-id-completed id)))
|
|
response))))
|
|
|
|
(defun nrepl-request:stdin (input callback connection)
|
|
"Send a :stdin request with INPUT using CONNECTION.
|
|
Register CALLBACK as the response handler."
|
|
(nrepl-send-request `("op" "stdin"
|
|
"stdin" ,input)
|
|
callback
|
|
connection))
|
|
|
|
(defun nrepl-request:interrupt (pending-request-id callback connection)
|
|
"Send an :interrupt request for PENDING-REQUEST-ID.
|
|
The request is dispatched using CONNECTION.
|
|
Register CALLBACK as the response handler."
|
|
(nrepl-send-request `("op" "interrupt"
|
|
"interrupt-id" ,pending-request-id)
|
|
callback
|
|
connection))
|
|
|
|
(define-minor-mode cider-enlighten-mode nil nil (cider-mode " light")
|
|
:global t)
|
|
|
|
(defun nrepl--eval-request (input &optional ns line column)
|
|
"Prepare :eval request message for INPUT.
|
|
NS provides context for the request.
|
|
If LINE and COLUMN are non-nil and current buffer is a file buffer, \"line\",
|
|
\"column\" and \"file\" are added to the message."
|
|
(nconc (and ns `("ns" ,ns))
|
|
`("op" "eval"
|
|
"code" ,(substring-no-properties input))
|
|
(when cider-enlighten-mode
|
|
'("enlighten" "true"))
|
|
(let ((file (or (buffer-file-name) (buffer-name))))
|
|
(when (and line column file)
|
|
`("file" ,file
|
|
"line" ,line
|
|
"column" ,column)))))
|
|
|
|
(defun nrepl-request:eval (input callback connection &optional ns line column additional-params tooling)
|
|
"Send the request INPUT and register the CALLBACK as the response handler.
|
|
The request is dispatched via CONNECTION. If NS is non-nil,
|
|
include it in the request. LINE and COLUMN, if non-nil, define the position
|
|
of INPUT in its buffer. A CONNECTION uniquely determines two connections
|
|
available: the standard interaction one and the tooling session. If the
|
|
tooling is desired, set TOOLING to true.
|
|
ADDITIONAL-PARAMS is a plist to be appended to the request message."
|
|
(nrepl-send-request (append (nrepl--eval-request input ns line column) additional-params)
|
|
callback
|
|
connection
|
|
tooling))
|
|
|
|
(defun nrepl-sync-request:clone (connection &optional tooling)
|
|
"Sent a :clone request to create a new client session.
|
|
The request is dispatched via CONNECTION.
|
|
Optional argument TOOLING Tooling is set to t if wanting the tooling session from CONNECTION."
|
|
(nrepl-send-sync-request '("op" "clone")
|
|
connection
|
|
nil tooling))
|
|
|
|
(defun nrepl-sync-request:close (connection)
|
|
"Sent a :close request to close CONNECTION's SESSION."
|
|
(nrepl-send-sync-request '("op" "close") connection)
|
|
(nrepl-send-sync-request '("op" "close") connection nil t)) ;; close tooling session
|
|
|
|
(defun nrepl-sync-request:describe (connection)
|
|
"Perform :describe request for CONNECTION and SESSION."
|
|
(nrepl-send-sync-request '("op" "describe")
|
|
connection))
|
|
|
|
(defun nrepl-sync-request:ls-sessions (connection)
|
|
"Perform :ls-sessions request for CONNECTION."
|
|
(nrepl-send-sync-request '("op" "ls-sessions") connection))
|
|
|
|
(defun nrepl-sync-request:eval (input connection &optional ns tooling)
|
|
"Send the INPUT to the nREPL server synchronously.
|
|
The request is dispatched via CONNECTION.
|
|
If NS is non-nil, include it in the request
|
|
If TOOLING is non-nil the evaluation is done using the tooling nREPL
|
|
session."
|
|
(nrepl-send-sync-request
|
|
(nrepl--eval-request input ns)
|
|
connection
|
|
nil
|
|
tooling))
|
|
|
|
(defun nrepl-sessions (connection)
|
|
"Get a list of active sessions on the nREPL server using CONNECTION."
|
|
(nrepl-dict-get (nrepl-sync-request:ls-sessions connection) "sessions"))
|
|
|
|
|
|
;;; Server
|
|
|
|
;; The server side process is started by `nrepl-start-server-process' and has a
|
|
;; very simple filter that pipes its output directly into its process buffer
|
|
;; (*nrepl-server*). The main purpose of this process is to start the actual
|
|
;; nrepl communication client (`nrepl-client-filter') when the message "nREPL
|
|
;; server started on port ..." is detected.
|
|
|
|
;; internal variables used for state transfer between nrepl-start-server-process
|
|
;; and nrepl-server-filter.
|
|
(defvar-local nrepl-on-port-callback nil)
|
|
|
|
(defun nrepl-server-p (buffer-or-process)
|
|
"Return t if BUFFER-OR-PROCESS is an nREPL server."
|
|
(let ((buffer (if (processp buffer-or-process)
|
|
(process-buffer buffer-or-process)
|
|
buffer-or-process)))
|
|
(buffer-local-value 'nrepl-is-server buffer)))
|
|
|
|
(defun nrepl-start-server-process (directory cmd on-port-callback)
|
|
"Start nREPL server process in DIRECTORY using shell command CMD.
|
|
Return a newly created process. Set `nrepl-server-filter' as the process
|
|
filter, which starts REPL process with its own buffer once the server has
|
|
started. ON-PORT-CALLBACK is a function of one argument (server buffer)
|
|
which is called by the process filter once the port of the connection has
|
|
been determined."
|
|
(let* ((default-directory (or directory default-directory))
|
|
(serv-buf (get-buffer-create
|
|
(nrepl-server-buffer-name
|
|
`(:project-dir ,default-directory)))))
|
|
(with-current-buffer serv-buf
|
|
(setq nrepl-is-server t
|
|
nrepl-project-dir default-directory
|
|
nrepl-server-command cmd
|
|
nrepl-on-port-callback on-port-callback))
|
|
(let ((serv-proc (start-file-process-shell-command
|
|
"nrepl-server" serv-buf cmd)))
|
|
(set-process-filter serv-proc 'nrepl-server-filter)
|
|
(set-process-sentinel serv-proc 'nrepl-server-sentinel)
|
|
(set-process-coding-system serv-proc 'utf-8-unix 'utf-8-unix)
|
|
(message "[nREPL] Starting server via %s"
|
|
(propertize cmd 'face 'font-lock-keyword-face))
|
|
serv-proc)))
|
|
|
|
(defun nrepl-server-filter (process output)
|
|
"Process nREPL server output from PROCESS contained in OUTPUT."
|
|
;; In Windows this can be false:
|
|
(let ((server-buffer (process-buffer process)))
|
|
(when (buffer-live-p server-buffer)
|
|
(with-current-buffer server-buffer
|
|
;; auto-scroll on new output
|
|
(let ((moving (= (point) (process-mark process))))
|
|
(save-excursion
|
|
(goto-char (process-mark process))
|
|
(insert output)
|
|
(ansi-color-apply-on-region (process-mark process) (point))
|
|
(set-marker (process-mark process) (point)))
|
|
(when moving
|
|
(goto-char (process-mark process))
|
|
(when-let* ((win (get-buffer-window)))
|
|
(set-window-point win (point)))))
|
|
;; detect the port the server is listening on from its output
|
|
(when (and (null nrepl-endpoint)
|
|
(string-match "nREPL server started on port \\([0-9]+\\)" output))
|
|
(let ((port (string-to-number (match-string 1 output))))
|
|
(setq nrepl-endpoint (list :host (or (file-remote-p default-directory 'host)
|
|
"localhost")
|
|
:port port))
|
|
(message "[nREPL] server started on %s" port)
|
|
(when nrepl-on-port-callback
|
|
(funcall nrepl-on-port-callback (process-buffer process)))))))))
|
|
|
|
(declare-function cider--close-connection "cider-connection")
|
|
(defun nrepl-server-sentinel (process event)
|
|
"Handle nREPL server PROCESS EVENT."
|
|
(let* ((server-buffer (process-buffer process))
|
|
(clients (seq-filter (lambda (b)
|
|
(eq (buffer-local-value 'nrepl-server-buffer b)
|
|
server-buffer))
|
|
(buffer-list)))
|
|
(problem (if (and server-buffer (buffer-live-p server-buffer))
|
|
(with-current-buffer server-buffer
|
|
(buffer-substring (point-min) (point-max)))
|
|
"")))
|
|
(when server-buffer
|
|
(kill-buffer server-buffer))
|
|
(cond
|
|
((string-match-p "^killed\\|^interrupt" event)
|
|
nil)
|
|
((string-match-p "^hangup" event)
|
|
(mapc #'cider--close-connection clients))
|
|
;; On Windows, a failed start sends the "finished" event. On Linux it sends
|
|
;; "exited abnormally with code 1".
|
|
(t (error "Could not start nREPL server: %s" problem)))))
|
|
|
|
|
|
;;; Messages
|
|
|
|
(defcustom nrepl-log-messages nil
|
|
"If non-nil, log protocol messages to an nREPL messages buffer.
|
|
This is extremely useful for debug purposes, as it allows you to inspect
|
|
the communication between Emacs and an nREPL server. Enabling the logging
|
|
might have a negative impact on performance, so it's not recommended to
|
|
keep it enabled unless you need to debug something."
|
|
:type 'boolean
|
|
:group 'nrepl
|
|
:safe #'booleanp)
|
|
|
|
(defconst nrepl-message-buffer-max-size 1000000
|
|
"Maximum size for the nREPL message buffer.
|
|
Defaults to 1000000 characters, which should be an insignificant
|
|
memory burden, while providing reasonable history.")
|
|
|
|
(defconst nrepl-message-buffer-reduce-denominator 4
|
|
"Divisor by which to reduce message buffer size.
|
|
When the maximum size for the nREPL message buffer is exceeded, the size of
|
|
the buffer is reduced by one over this value. Defaults to 4, so that 1/4
|
|
of the buffer is removed, which should ensure the buffer's maximum is
|
|
reasonably utilized, while limiting the number of buffer shrinking
|
|
operations.")
|
|
|
|
(defvar nrepl-messages-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map (kbd "n") #'next-line)
|
|
(define-key map (kbd "p") #'previous-line)
|
|
(define-key map (kbd "TAB") #'forward-button)
|
|
(define-key map (kbd "RET") #'nrepl-log-expand-button)
|
|
(define-key map (kbd "e") #'nrepl-log-expand-button)
|
|
(define-key map (kbd "E") #'nrepl-log-expand-all-buttons)
|
|
(define-key map (kbd "<backtab>") #'backward-button)
|
|
map))
|
|
|
|
(define-derived-mode nrepl-messages-mode special-mode "nREPL Messages"
|
|
"Major mode for displaying nREPL messages.
|
|
|
|
\\{nrepl-messages-mode-map}"
|
|
(when cider-special-mode-truncate-lines
|
|
(setq-local truncate-lines t))
|
|
(setq-local sesman-system 'CIDER)
|
|
(setq-local electric-indent-chars nil)
|
|
(setq-local comment-start ";")
|
|
(setq-local comment-end "")
|
|
(setq-local paragraph-start "(-->\\|(<--")
|
|
(setq-local paragraph-separate "(<--"))
|
|
|
|
(defun nrepl-decorate-msg (msg type)
|
|
"Decorate nREPL MSG according to its TYPE."
|
|
(pcase type
|
|
(`request (cons '--> (cdr msg)))
|
|
(`response (cons '<-- (cdr msg)))))
|
|
|
|
(defun nrepl-log-message (msg type)
|
|
"Log the nREPL MSG.
|
|
TYPE is either request or response. The message is logged to a buffer
|
|
described by `nrepl-message-buffer-name-template'."
|
|
(when nrepl-log-messages
|
|
;; append a time-stamp to the message before logging it
|
|
;; the time-stamps are quite useful for debugging
|
|
(setq msg (cons (car msg)
|
|
(lax-plist-put (cdr msg) "time-stamp"
|
|
(format-time-string "%Y-%m-%0d %H:%M:%S.%N"))))
|
|
(with-current-buffer (nrepl-messages-buffer (current-buffer))
|
|
(setq buffer-read-only nil)
|
|
(when (> (buffer-size) nrepl-message-buffer-max-size)
|
|
(goto-char (/ (buffer-size) nrepl-message-buffer-reduce-denominator))
|
|
(re-search-forward "^(" nil t)
|
|
(delete-region (point-min) (- (point) 1)))
|
|
(goto-char (point-max))
|
|
(nrepl-log-pp-object (nrepl-decorate-msg msg type)
|
|
(nrepl-log--message-color (lax-plist-get (cdr msg) "id"))
|
|
t)
|
|
(when-let* ((win (get-buffer-window)))
|
|
(set-window-point win (point-max)))
|
|
(setq buffer-read-only t))))
|
|
|
|
(defun nrepl-toggle-message-logging ()
|
|
"Toggle the value of `nrepl-log-messages' between nil and t.
|
|
|
|
This in effect enables or disables the logging of nREPL messages."
|
|
(interactive)
|
|
(setq nrepl-log-messages (not nrepl-log-messages))
|
|
(if nrepl-log-messages
|
|
(message "nREPL message logging enabled")
|
|
(message "nREPL message logging disabled")))
|
|
|
|
(defcustom nrepl-message-colors
|
|
'("red" "brown" "coral" "orange" "green" "deep sky blue" "blue" "dark violet")
|
|
"Colors used in the messages buffer."
|
|
:type '(repeat color)
|
|
:group 'nrepl)
|
|
|
|
(defun nrepl-log-expand-button (&optional button)
|
|
"Expand the objects hidden in BUTTON's :nrepl-object property.
|
|
BUTTON defaults the button at point."
|
|
(interactive)
|
|
(if-let* ((button (or button (button-at (point)))))
|
|
(let* ((start (overlay-start button))
|
|
(end (overlay-end button))
|
|
(obj (overlay-get button :nrepl-object))
|
|
(inhibit-read-only t))
|
|
(save-excursion
|
|
(goto-char start)
|
|
(delete-overlay button)
|
|
(delete-region start end)
|
|
(nrepl-log-pp-object obj)
|
|
(delete-char -1)))
|
|
(error "No button at point")))
|
|
|
|
(defun nrepl-log-expand-all-buttons ()
|
|
"Expand all buttons in nREPL log buffer."
|
|
(interactive)
|
|
(if (not (eq major-mode 'nrepl-messages-mode))
|
|
(user-error "Not in a `nrepl-messages-mode'")
|
|
(save-excursion
|
|
(let* ((pos (point-min))
|
|
(button (next-button pos)))
|
|
(while button
|
|
(setq pos (overlay-start button))
|
|
(nrepl-log-expand-button button)
|
|
(setq button (next-button pos)))))))
|
|
|
|
(defun nrepl-log--expand-button-mouse (event)
|
|
"Expand the text hidden under overlay button.
|
|
EVENT gives the button position on window."
|
|
(interactive "e")
|
|
(pcase (elt event 1)
|
|
(`(,window ,_ ,_ ,_ ,_ ,point . ,_)
|
|
(with-selected-window window
|
|
(nrepl-log-expand-button (button-at point))))))
|
|
|
|
(defun nrepl-log-insert-button (label object)
|
|
"Insert button with LABEL and :nrepl-object property as OBJECT."
|
|
(insert-button label
|
|
:nrepl-object object
|
|
'action #'nrepl-log-expand-button
|
|
'face 'link
|
|
'help-echo "RET: Expand object."
|
|
;; Workaround for bug#1568 (don't use local-map here; it
|
|
;; overwrites major mode map.)
|
|
'keymap `(keymap (mouse-1 . nrepl-log--expand-button-mouse)))
|
|
(insert "\n"))
|
|
|
|
(defun nrepl-log--message-color (id)
|
|
"Return the color to use when pretty-printing the nREPL message with ID.
|
|
If ID is nil, return nil."
|
|
(when id
|
|
(thread-first (string-to-number id)
|
|
(mod (length nrepl-message-colors))
|
|
(nth nrepl-message-colors))))
|
|
|
|
(defun nrepl-log--pp-listlike (object &optional foreground button)
|
|
"Pretty print nREPL list like OBJECT.
|
|
FOREGROUND and BUTTON are as in `nrepl-log-pp-object'."
|
|
(cl-flet ((color (str)
|
|
(propertize str 'face
|
|
(append '(:weight ultra-bold)
|
|
(when foreground `(:foreground ,foreground))))))
|
|
(let ((head (format "(%s" (car object))))
|
|
(insert (color head))
|
|
(if (null (cdr object))
|
|
(insert ")\n")
|
|
(let* ((indent (+ 2 (- (current-column) (length head))))
|
|
(sorted-pairs (sort (seq-partition (cl-copy-list (cdr object)) 2)
|
|
(lambda (a b)
|
|
(string< (car a) (car b)))))
|
|
(name-lengths (seq-map (lambda (pair) (length (car pair))) sorted-pairs))
|
|
(longest-name (seq-max name-lengths))
|
|
;; Special entries are displayed first
|
|
(specialq (lambda (pair) (seq-contains '("id" "op" "session" "time-stamp") (car pair))))
|
|
(special-pairs (seq-filter specialq sorted-pairs))
|
|
(not-special-pairs (seq-remove specialq sorted-pairs))
|
|
(all-pairs (seq-concatenate 'list special-pairs not-special-pairs))
|
|
(sorted-object (apply 'seq-concatenate 'list all-pairs)))
|
|
(insert "\n")
|
|
(cl-loop for l on sorted-object by #'cddr
|
|
do (let ((indent-str (make-string indent ?\s))
|
|
(name-str (propertize (car l) 'face
|
|
;; Only highlight top-level keys.
|
|
(unless (eq (car object) 'dict)
|
|
'font-lock-keyword-face)))
|
|
(spaces-str (make-string (- longest-name (length (car l))) ?\s)))
|
|
(insert (format "%s%s%s " indent-str name-str spaces-str))
|
|
(nrepl-log-pp-object (cadr l) nil button)))
|
|
(when (eq (car object) 'dict)
|
|
(delete-char -1))
|
|
(insert (color ")\n")))))))
|
|
|
|
(defun nrepl-log-pp-object (object &optional foreground button)
|
|
"Pretty print nREPL OBJECT, delimited using FOREGROUND.
|
|
If BUTTON is non-nil, try making a button from OBJECT instead of inserting
|
|
it into the buffer."
|
|
(let ((min-dict-fold-size 1)
|
|
(min-list-fold-size 10)
|
|
(min-string-fold-size 60))
|
|
(if-let* ((head (car-safe object)))
|
|
;; list-like objects
|
|
(cond
|
|
;; top level dicts (always expanded)
|
|
((memq head '(<-- -->))
|
|
(nrepl-log--pp-listlike object foreground button))
|
|
;; inner dicts
|
|
((eq head 'dict)
|
|
(if (and button (> (length object) min-dict-fold-size))
|
|
(nrepl-log-insert-button "(dict ...)" object)
|
|
(nrepl-log--pp-listlike object foreground button)))
|
|
;; lists
|
|
(t
|
|
(if (and button (> (length object) min-list-fold-size))
|
|
(nrepl-log-insert-button (format "(%s ...)" (prin1-to-string head)) object)
|
|
(pp object (current-buffer)))))
|
|
;; non-list objects
|
|
(if (stringp object)
|
|
(if (and button (> (length object) min-string-fold-size))
|
|
(nrepl-log-insert-button (format "\"%s...\"" (substring object 0 min-string-fold-size)) object)
|
|
(insert (prin1-to-string object) "\n"))
|
|
(pp object (current-buffer))
|
|
(insert "\n")))))
|
|
|
|
(defun nrepl-messages-buffer (conn)
|
|
"Return or create the buffer for CONN.
|
|
The default buffer name is *nrepl-messages connection*."
|
|
(with-current-buffer conn
|
|
(or (and (buffer-live-p nrepl-messages-buffer)
|
|
nrepl-messages-buffer)
|
|
(setq nrepl-messages-buffer
|
|
(let ((buffer (get-buffer-create
|
|
(nrepl-messages-buffer-name
|
|
(cider--gather-connect-params)))))
|
|
(with-current-buffer buffer
|
|
(buffer-disable-undo)
|
|
(nrepl-messages-mode)
|
|
buffer))))))
|
|
|
|
(defun nrepl-error-buffer ()
|
|
"Return or create the buffer.
|
|
The default buffer name is *nrepl-error*."
|
|
(or (get-buffer nrepl-error-buffer-name)
|
|
(let ((buffer (get-buffer-create nrepl-error-buffer-name)))
|
|
(with-current-buffer buffer
|
|
(buffer-disable-undo)
|
|
(fundamental-mode)
|
|
buffer))))
|
|
|
|
(defun nrepl-log-error (msg)
|
|
"Log the given MSG to the buffer given by `nrepl-error-buffer'."
|
|
(with-current-buffer (nrepl-error-buffer)
|
|
(setq buffer-read-only nil)
|
|
(goto-char (point-max))
|
|
(insert msg)
|
|
(when-let* ((win (get-buffer-window)))
|
|
(set-window-point win (point-max)))
|
|
(setq buffer-read-only t)))
|
|
|
|
(make-obsolete 'nrepl-default-client-buffer-builder nil "0.18")
|
|
|
|
(provide 'nrepl-client)
|
|
|
|
;;; nrepl-client.el ends here
|