|
|
;;; nrepl-client.el --- Client for Clojure nREPL -*- lexical-binding: t -*- |
|
|
|
|
|
;; Copyright © 2012-2015 Tim King, Phil Hagelberg |
|
|
;; Copyright © 2013-2015 Bozhidar Batsov, Hugo Duncan, Steve Purcell |
|
|
;; |
|
|
;; Author: Tim King <kingtim@gmail.com> |
|
|
;; Phil Hagelberg <technomancy@gmail.com> |
|
|
;; Bozhidar Batsov <bozhidar@batsov.com> |
|
|
;; Hugo Duncan <hugo@hugoduncan.org> |
|
|
;; Steve Purcell <steve@sanityinc.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 'cider-compat) |
|
|
|
|
|
(require 'thingatpt) |
|
|
(require 'etags) |
|
|
(require 'ansi-color) |
|
|
(require 'ewoc) |
|
|
(require 'cl-lib) |
|
|
(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) |
|
|
|
|
|
(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) |
|
|
|
|
|
(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-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) |
|
|
|
|
|
(defvar nrepl-create-client-buffer-function 'nrepl-create-client-buffer-default |
|
|
"Name of a function that returns a client process buffer. |
|
|
It is called with one argument, a plist containing :host, :port and :proc |
|
|
as returned by `nrepl-connect'.") |
|
|
|
|
|
(defvar nrepl-use-this-as-repl-buffer 'new |
|
|
"Name of the buffer to use as REPL buffer. |
|
|
In case of a special value 'new, a new buffer is created.") |
|
|
|
|
|
|
|
|
;;; Buffer Local Declarations |
|
|
|
|
|
;; These variables are used to track the state of nREPL connections |
|
|
(defvar-local nrepl-client-buffers nil |
|
|
"List of buffers connected to this server.") |
|
|
(defvar-local nrepl-connection-buffer nil) |
|
|
(define-obsolete-variable-alias 'nrepl-repl-buffer |
|
|
'nrepl-connection-buffer "0.10.0") |
|
|
(defvar-local nrepl-server-buffer nil) |
|
|
(defvar-local nrepl-endpoint nil) |
|
|
(defvar-local nrepl-project-dir 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.") |
|
|
|
|
|
|
|
|
;;; nREPL Buffer Names |
|
|
|
|
|
(defconst nrepl-message-buffer-name "*nrepl-messages*") |
|
|
(defconst nrepl-repl-buffer-name-template "*cider-repl%s*") |
|
|
(defconst nrepl-connection-buffer-name-template "*nrepl-connection%s*") |
|
|
(defconst nrepl-server-buffer-name-template "*nrepl-server%s*") |
|
|
(defconst nrepl-tunnel-buffer-name-template "*nrepl-tunnel%s*") |
|
|
|
|
|
(defun nrepl-format-buffer-name-template (buffer-name-template designation) |
|
|
"Apply the DESIGNATION to the corresponding BUFFER-NAME-TEMPLATE." |
|
|
(format buffer-name-template |
|
|
(if (> (length designation) 0) |
|
|
(concat nrepl-buffer-name-separator designation) |
|
|
""))) |
|
|
|
|
|
(defun nrepl-make-buffer-name (buffer-name-template &optional project-dir host port dup-ok) |
|
|
"Generate a buffer name using BUFFER-NAME-TEMPLATE. |
|
|
|
|
|
If not supplied PROJECT-DIR, HOST and PORT default to the buffer local |
|
|
value of the `nrepl-project-dir' and `nrepl-endpoint'. |
|
|
|
|
|
The name will include the project name if available or the endpoint host if |
|
|
it is not. The name will also include the connection port if |
|
|
`nrepl-buffer-name-show-port' is true. |
|
|
|
|
|
If optional DUP-OK is non-nil, the returned buffer is not \"uniquified\" by |
|
|
`generate-new-buffer-name'." |
|
|
(let* ((project-dir (or project-dir nrepl-project-dir)) |
|
|
(project-name (when project-dir (file-name-nondirectory (directory-file-name project-dir)))) |
|
|
(nrepl-proj-port (or port (cadr nrepl-endpoint))) |
|
|
(name (nrepl-format-buffer-name-template |
|
|
buffer-name-template |
|
|
(concat (if project-name project-name (or host (car nrepl-endpoint))) |
|
|
(if (and nrepl-proj-port nrepl-buffer-name-show-port) |
|
|
(format ":%s" nrepl-proj-port) ""))))) |
|
|
(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-connection-buffer-name (&optional project-dir host port) |
|
|
"Return the name of the connection buffer. |
|
|
PROJECT-DIR, HOST and PORT are as in `/nrepl-make-buffer-name'." |
|
|
(nrepl--make-hidden-name |
|
|
(nrepl-make-buffer-name nrepl-connection-buffer-name-template |
|
|
project-dir host port))) |
|
|
|
|
|
(defun nrepl-server-buffer-name (&optional project-dir host port) |
|
|
"Return the name of the server buffer. |
|
|
PROJECT-DIR, HOST and PORT are as in `nrepl-make-buffer-name'." |
|
|
(nrepl--make-hidden-name |
|
|
(nrepl-make-buffer-name nrepl-server-buffer-name-template |
|
|
project-dir host port))) |
|
|
|
|
|
(defun nrepl-tunnel-buffer-name (&optional project-dir host port) |
|
|
"Return the name of the tunnel buffer. |
|
|
PROJECT-DIR, HOST and PORT are as in `nrepl-make-buffer-name'." |
|
|
(nrepl--make-hidden-name |
|
|
(nrepl-make-buffer-name nrepl-tunnel-buffer-name-template |
|
|
project-dir host port))) |
|
|
|
|
|
|
|
|
;;; Utilities |
|
|
(defmacro nrepl-dbind-response (response keys &rest body) |
|
|
"Destructure an nREPL RESPONSE dict. |
|
|
Bind the value of the provided KEYS and execute BODY." |
|
|
(declare (debug (form (&rest symbolp) body))) |
|
|
`(let ,(cl-loop for key in keys |
|
|
collect `(,key (nrepl-dict-get ,response ,(format "%s" key)))) |
|
|
,@body)) |
|
|
(put 'nrepl-dbind-response 'lisp-indent-function 2) |
|
|
|
|
|
(defun nrepl-op-supported-p (op connection) |
|
|
"Return t iff the given operation OP is supported by the nREPL CONNECTION." |
|
|
(with-current-buffer connection |
|
|
(and nrepl-ops (nrepl-dict-get nrepl-ops op)))) |
|
|
|
|
|
(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)))) |
|
|
|
|
|
(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)))) |
|
|
|
|
|
|
|
|
;;; nREPL dict |
|
|
|
|
|
(defun nrepl-dict (&rest key-vals) |
|
|
"Create nREPL dict from KEY-VALS." |
|
|
(cons 'dict key-vals)) |
|
|
|
|
|
(defun nrepl-dict-p (object) |
|
|
"Return t if OBJECT is a nREPL dict." |
|
|
(and (listp object) |
|
|
(eq (car object) 'dict))) |
|
|
|
|
|
(defun nrepl-dict-empty-p (dict) |
|
|
"Return t if nREPL dict DICT is empty." |
|
|
(null (cdr dict))) |
|
|
|
|
|
(defun nrepl-dict-get (dict key) |
|
|
"Get from DICT value associated with KEY. |
|
|
If dict is nil, return nil." |
|
|
(when dict |
|
|
(if (nrepl-dict-p dict) |
|
|
(lax-plist-get (cdr dict) key) |
|
|
(error "Not a nREPL dict object: %s" dict)))) |
|
|
|
|
|
(defun nrepl-dict-put (dict key value) |
|
|
"Associate in DICT, KEY to VALUE. |
|
|
Return new dict. Dict is modified by side effects." |
|
|
(if (null dict) |
|
|
(list 'dict key value) |
|
|
(if (not (nrepl-dict-p dict)) |
|
|
(error "Not a nREPL dict object: %s" dict) |
|
|
(setcdr dict (lax-plist-put (cdr dict) key value)) |
|
|
dict))) |
|
|
|
|
|
(defun nrepl-dict-keys (dict) |
|
|
"Return all the keys in the nREPL DICT." |
|
|
(if (nrepl-dict-p dict) |
|
|
(cl-loop for l on (cdr dict) by #'cddr |
|
|
collect (car l)) |
|
|
(error "Not a nREPL dict"))) |
|
|
|
|
|
(defun nrepl-dict-vals (dict) |
|
|
"Return all the values in the nREPL DICT." |
|
|
(if (nrepl-dict-p dict) |
|
|
(cl-loop for l on (cdr dict) by #'cddr |
|
|
collect (cadr l)) |
|
|
(error "Not a nREPL dict"))) |
|
|
|
|
|
(defun nrepl-dict-map (fn dict) |
|
|
"Map FN on nREPL DICT. |
|
|
FN must accept two arguments key and value." |
|
|
(if (nrepl-dict-p dict) |
|
|
(cl-loop for l on (cdr dict) by #'cddr |
|
|
collect (funcall fn (car l) (cadr l))) |
|
|
(error "Not a nREPL dict"))) |
|
|
|
|
|
(defun nrepl-dict-merge (dict1 dict2) |
|
|
"Destructively merge DICT2 into DICT1. |
|
|
Keys in DICT2 override those in DICT1." |
|
|
(let ((base (or dict1 '(dict)))) |
|
|
(nrepl-dict-map (lambda (k v) |
|
|
(nrepl-dict-put base k v)) |
|
|
(or dict2 '(dict))) |
|
|
base)) |
|
|
|
|
|
(defun nrepl-dict-get-in (dict keys) |
|
|
"Return the value in a nested DICT. |
|
|
KEYS is a list of keys. Return nil if any of the keys is not present or if |
|
|
any of the values is nil." |
|
|
(let ((out dict)) |
|
|
(while (and keys out) |
|
|
(setq out (nrepl-dict-get out (pop keys)))) |
|
|
out)) |
|
|
|
|
|
(defun nrepl-dict-flat-map (function dict) |
|
|
"Map FUNCTION over DICT and flatten the result. |
|
|
FUNCTION follows the same restrictions as in `nrepl-dict-map', and it must |
|
|
also alway return a sequence (since the result will be flattened)." |
|
|
(when dict |
|
|
(apply #'append (nrepl-dict-map function dict)))) |
|
|
|
|
|
(defun nrepl--cons (car list-or-dict) |
|
|
"Generic cons of CAR to LIST-OR-DICT." |
|
|
(if (eq (car list-or-dict) 'dict) |
|
|
(cons 'dict (cons car (cdr list-or-dict))) |
|
|
(cons car list-or-dict))) |
|
|
|
|
|
(defun nrepl--nreverse (list-or-dict) |
|
|
"Generic `nreverse' which works on LIST-OR-DICT." |
|
|
(if (eq (car list-or-dict) 'dict) |
|
|
(cons 'dict (nreverse (cdr list-or-dict))) |
|
|
(nreverse list-or-dict))) |
|
|
|
|
|
(defun nrepl--push (obj stack) |
|
|
"Cons OBJ to the top element of the STACK." |
|
|
;; stack is assumed to be a list |
|
|
(if (eq (caar stack) 'dict) |
|
|
(cons (cons 'dict (cons obj (cdar stack))) |
|
|
(cdr stack)) |
|
|
(cons (if (null stack) |
|
|
obj |
|
|
(cons obj (car stack))) |
|
|
(cdr stack)))) |
|
|
|
|
|
(defun nrepl--merge (dict1 dict2 &optional no-join) |
|
|
"Join nREPL dicts DICT1 and DICT2 in a meaningful way. |
|
|
String values for non \"id\" and \"session\" keys are concatenated. Lists |
|
|
are appended. nREPL dicts merged recursively. All other objects are |
|
|
accumulated into a list. DICT1 is modified destructively and |
|
|
then returned." |
|
|
(if no-join |
|
|
(or dict1 dict2) |
|
|
(cond ((null dict1) dict2) |
|
|
((null dict2) dict1) |
|
|
((stringp dict1) (concat dict1 dict2)) |
|
|
((nrepl-dict-p dict1) |
|
|
(nrepl-dict-map |
|
|
(lambda (k2 v2) |
|
|
(nrepl-dict-put dict1 k2 |
|
|
(nrepl--merge (nrepl-dict-get dict1 k2) v2 |
|
|
(member k2 '("id" "session"))))) |
|
|
dict2) |
|
|
dict1) |
|
|
((and (listp dict2) (listp dict1)) (append dict1 dict2)) |
|
|
((listp dict1) (append dict1 (list dict2))) |
|
|
(t (list dict1 dict2))))) |
|
|
|
|
|
|
|
|
;;; 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" "" 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 %s buffer." |
|
|
nrepl-message-buffer-name) |
|
|
(nrepl-log-message |
|
|
(format "Decoder error at position %d (`%s'):" |
|
|
(point) (buffer-substring (point) (min (+ (point) 10) (point-max))))) |
|
|
(nrepl-log-message (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-temp-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)) |
|
|
(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." |
|
|
;; (nrepl-log-message string) |
|
|
(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 (cons '<- (cdr 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))) |
|
|
(with-current-buffer client-buffer |
|
|
(run-hooks 'nrepl-disconnected-hook) |
|
|
(when (buffer-live-p nrepl-server-buffer) |
|
|
(with-current-buffer nrepl-server-buffer |
|
|
(setq nrepl-client-buffers (delete client-buffer nrepl-client-buffers))) |
|
|
(nrepl--maybe-kill-server-buffer nrepl-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, 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) |
|
|
(or (and host (not nrepl-force-ssh-for-remote-hosts) |
|
|
(nrepl--direct-connect host port 'no-error)) |
|
|
(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 |
|
|
(error "Host (%s) and port (%s) must be provided" host port)) |
|
|
(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 established")) |
|
|
(error (let ((mes "nREPL: Direct connection failed")) |
|
|
(if no-error (message mes) (error mes)) |
|
|
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 ...") |
|
|
(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)) |
|
|
(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 nil |
|
|
;; 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 . ,host) |
|
|
(?u . ,(if user (format "-l '%s' " 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--maybe-kill-server-buffer (server-buf) |
|
|
"Kill SERVER-BUFFER and its process, subject to user confirmation. |
|
|
Do nothing if there is a REPL connected to that server." |
|
|
(with-current-buffer server-buf |
|
|
;; Don't kill the server if there is a REPL connected to it. |
|
|
(when (and (not nrepl-client-buffers) |
|
|
(y-or-n-p "Also kill server process and buffer? ")) |
|
|
(let ((proc (get-buffer-process server-buf))) |
|
|
(when (process-live-p proc) |
|
|
(set-process-query-on-exit-flag proc nil) |
|
|
(kill-process proc)) |
|
|
(kill-buffer server-buf))))) |
|
|
|
|
|
;; `nrepl-start-client-process' is called from `nrepl-server-filter'. It |
|
|
;; starts the client process described by `nrepl-client-filter' and |
|
|
;; `nrepl-client-sentinel'. |
|
|
(defun nrepl-start-client-process (&optional host port server-proc) |
|
|
"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. This function creates connection buffer by a call to |
|
|
`nrepl-create-client-buffer-function'. Return newly created client |
|
|
process." |
|
|
(let* ((endpoint (nrepl-connect host port)) |
|
|
(client-proc (plist-get endpoint :proc)) |
|
|
(host (plist-get endpoint :host)) |
|
|
(port (plist-get endpoint :port)) |
|
|
(client-buf (funcall nrepl-create-client-buffer-function 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 `(,host ,port) |
|
|
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))) |
|
|
(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) |
|
|
(with-current-buffer conn-buffer |
|
|
(setq nrepl-ops ops) |
|
|
(setq nrepl-versions versions))))) |
|
|
|
|
|
(define-obsolete-function-alias 'nrepl-close 'cider--close-connection-buffer "0.10.0") |
|
|
|
|
|
;;; 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))) |
|
|
|
|
|
(defvar cider-buffer-ns) |
|
|
(declare-function cider-need-input "cider-interaction") |
|
|
|
|
|
(defun nrepl-make-response-handler (buffer value-handler stdout-handler |
|
|
stderr-handler done-handler |
|
|
&optional eval-error-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, and |
|
|
EVAL-ERROR-HANDLER. 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. |
|
|
|
|
|
When `nrepl-log-messages' is non-nil, *nrepl-messages* buffer contains |
|
|
server responses." |
|
|
(lambda (response) |
|
|
(nrepl-dbind-response response (value ns out err status id pprint-out) |
|
|
(when (buffer-live-p buffer) |
|
|
(with-current-buffer buffer |
|
|
(when (and ns (not (derived-mode-p 'clojure-mode))) |
|
|
(setq cider-buffer-ns ns)))) |
|
|
(cond (value |
|
|
(when value-handler |
|
|
(funcall value-handler buffer value))) |
|
|
(out |
|
|
(when stdout-handler |
|
|
(funcall stdout-handler buffer out))) |
|
|
(pprint-out |
|
|
(when stdout-handler |
|
|
(funcall stdout-handler buffer pprint-out))) |
|
|
(err |
|
|
(when stderr-handler |
|
|
(funcall stderr-handler buffer err))) |
|
|
(status |
|
|
(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 not found.")) |
|
|
(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/clojure/tools.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) |
|
|
"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. |
|
|
Return the ID of the sent message." |
|
|
(let* ((id (nrepl-next-request-id connection)) |
|
|
(request (cons 'dict (lax-plist-put request "id" id))) |
|
|
(message (nrepl-bencode request))) |
|
|
(nrepl-log-message (cons '---> (cdr request))) |
|
|
(with-current-buffer connection |
|
|
(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") |
|
|
|
|
|
(defun nrepl-send-sync-request (request connection &optional abort-on-input) |
|
|
"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." |
|
|
(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) |
|
|
(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) |
|
|
(when-let ((ex (nrepl-dict-get response "ex")) |
|
|
(err (nrepl-dict-get response "err"))) |
|
|
(cider-repl-emit-interactive-stderr err) |
|
|
(message "%s" err)) |
|
|
(when-let ((id (nrepl-dict-get response "id"))) |
|
|
(with-current-buffer connection |
|
|
(nrepl--mark-id-completed id))) |
|
|
response))) |
|
|
|
|
|
(defun nrepl-request:stdin (input callback connection session) |
|
|
"Send a :stdin request with INPUT using CONNECTION and SESSION. |
|
|
Register CALLBACK as the response handler." |
|
|
(nrepl-send-request (list "op" "stdin" |
|
|
"stdin" input |
|
|
"session" session) |
|
|
callback |
|
|
connection)) |
|
|
|
|
|
(defun nrepl-request:interrupt (pending-request-id callback connection session) |
|
|
"Send an :interrupt request for PENDING-REQUEST-ID. |
|
|
The request is dispatched using CONNECTION and SESSION. |
|
|
Register CALLBACK as the response handler." |
|
|
(nrepl-send-request (list "op" "interrupt" |
|
|
"session" session |
|
|
"interrupt-id" pending-request-id) |
|
|
callback |
|
|
connection)) |
|
|
|
|
|
(defun nrepl--eval-request (input session &optional ns point) |
|
|
"Prepare :eval request message for INPUT. |
|
|
SESSION and NS provide context for the request. |
|
|
If POINT is non-nil and current buffer is a file buffer, \"point\" and |
|
|
\"file\" are added to the message." |
|
|
(append (and ns (list "ns" ns)) |
|
|
(list "op" "eval" |
|
|
"session" session |
|
|
"code" input) |
|
|
(when (and point (buffer-file-name)) |
|
|
(list "file" (buffer-file-name) |
|
|
"point" point)))) |
|
|
|
|
|
(defun nrepl-request:eval (input callback connection session &optional ns point) |
|
|
"Send the request INPUT and register the CALLBACK as the response handler. |
|
|
The request is dispatched via CONNECTION and SESSION. |
|
|
If NS is non-nil, include it in the request. POINT, if non-nil, is the |
|
|
position of INPUT in its buffer." |
|
|
(nrepl-send-request (nrepl--eval-request input session ns point) |
|
|
callback |
|
|
connection)) |
|
|
|
|
|
(defun nrepl-sync-request:clone (connection) |
|
|
"Sent a :clone request to create a new client session. |
|
|
The request is dispatched via CONNECTION." |
|
|
(nrepl-send-sync-request '("op" "clone") |
|
|
connection)) |
|
|
|
|
|
(defun nrepl-sync-request:close (connection session) |
|
|
"Sent a :close request to close CONNECTION's SESSION." |
|
|
(nrepl-send-sync-request (list "op" "close" "session" session) |
|
|
connection)) |
|
|
|
|
|
(defun nrepl-sync-request:describe (connection &optional session) |
|
|
"Perform :describe request for CONNECTION and SESSION." |
|
|
(if session |
|
|
(nrepl-send-sync-request (list "session" session "op" "describe") |
|
|
connection) |
|
|
(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 session &optional ns) |
|
|
"Send the INPUT to the nREPL server synchronously. |
|
|
The request is dispatched via CONNECTION and SESSION. |
|
|
If NS is non-nil, include it in the request." |
|
|
(nrepl-send-sync-request |
|
|
(nrepl--eval-request input session ns) |
|
|
connection)) |
|
|
|
|
|
(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. |
|
|
|
|
|
(defvar-local nrepl-post-client-callback nil |
|
|
"Function called after the client process is started. |
|
|
Used by `nrepl-start-server-process'.") |
|
|
|
|
|
(defun nrepl-start-server-process (directory cmd &optional 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. |
|
|
If CALLBACK is non-nil, it should be function of 3 arguments. Once the |
|
|
client process is started, the function is called with the server process, |
|
|
the port, and the client buffer." |
|
|
(let* ((default-directory (or directory default-directory)) |
|
|
(serv-buf (get-buffer-create (generate-new-buffer-name |
|
|
(nrepl-server-buffer-name directory)))) |
|
|
(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) |
|
|
(with-current-buffer serv-buf |
|
|
(setq nrepl-project-dir directory) |
|
|
(setq nrepl-post-client-callback callback) |
|
|
;; Ensure that `nrepl-start-client-process' sees right things. This |
|
|
;; causes warnings about making a local within a let-bind. This is safe |
|
|
;; as long as `serv-buf' is not the buffer where the let-binding was |
|
|
;; started. http://www.gnu.org/software/emacs/manual/html_node/elisp/Creating-Buffer_002dLocal.html |
|
|
(setq-local nrepl-create-client-buffer-function |
|
|
nrepl-create-client-buffer-function) |
|
|
(setq-local nrepl-use-this-as-repl-buffer |
|
|
nrepl-use-this-as-repl-buffer)) |
|
|
(message "Starting nREPL 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." |
|
|
(with-current-buffer (process-buffer process) |
|
|
;; auto-scroll on new output |
|
|
(let ((moving (= (point) (process-mark process)))) |
|
|
(save-excursion |
|
|
(goto-char (process-mark process)) |
|
|
(insert output) |
|
|
(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 (string-match "nREPL server started on port \\([0-9]+\\)" output) |
|
|
(let ((port (string-to-number (match-string 1 output)))) |
|
|
(message "nREPL server started on %s" port) |
|
|
(with-current-buffer (process-buffer process) |
|
|
(let* ((client-proc (nrepl-start-client-process nil port process)) |
|
|
(client-buffer (process-buffer client-proc))) |
|
|
(setq nrepl-client-buffers |
|
|
(cons client-buffer |
|
|
(delete client-buffer nrepl-client-buffers))) |
|
|
|
|
|
(when (functionp nrepl-post-client-callback) |
|
|
(funcall nrepl-post-client-callback client-buffer))))))) |
|
|
|
|
|
(declare-function cider--close-connection-buffer "cider-client") |
|
|
|
|
|
(defun nrepl-server-sentinel (process event) |
|
|
"Handle nREPL server PROCESS EVENT." |
|
|
(let* ((server-buffer (process-buffer process)) |
|
|
(clients (buffer-local-value 'nrepl-client-buffers server-buffer)) |
|
|
(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-buffer clients)) |
|
|
((string-match-p "Wrong number of arguments to repl task" problem) |
|
|
(error "Leiningen 2.x is required by CIDER")) |
|
|
;; 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 t |
|
|
"If non-nil, log protocol messages to the `nrepl-message-buffer-name' buffer." |
|
|
:type 'boolean |
|
|
:group 'nrepl) |
|
|
|
|
|
(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 "<backtab>") #'backward-button) |
|
|
map)) |
|
|
|
|
|
(define-derived-mode nrepl-messages-mode special-mode "nREPL Messages" |
|
|
"Major mode for displaying nREPL messages. |
|
|
|
|
|
\\{nrepl-messages-mode-map}" |
|
|
(setq buffer-read-only t) |
|
|
(setq-local truncate-lines t) |
|
|
(setq-local electric-indent-chars nil) |
|
|
(setq-local comment-start ";") |
|
|
(setq-local comment-end "") |
|
|
(setq-local paragraph-start "(--->\\|(<-") |
|
|
(setq-local paragraph-separate "(<-")) |
|
|
|
|
|
(defun nrepl-log-message (msg) |
|
|
"Log the given MSG to the buffer given by `nrepl-message-buffer-name'." |
|
|
(when nrepl-log-messages |
|
|
(with-current-buffer (nrepl-messages-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--pp msg (nrepl--message-color (lax-plist-get (cdr msg) "id"))) |
|
|
(when-let ((win (get-buffer-window))) |
|
|
(set-window-point win (point-max))) |
|
|
(setq buffer-read-only t)))) |
|
|
|
|
|
(defcustom nrepl-message-colors |
|
|
'("red" "brown" "coral" "orange" "green" "deep sky blue" "blue" "dark violet") |
|
|
"Colors used in `nrepl-messages-buffer'." |
|
|
:type '(repeat color) |
|
|
:group 'nrepl) |
|
|
|
|
|
(defun nrepl--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)))) |
|
|
|
|
|
(defcustom nrepl-dict-max-message-size 5 |
|
|
"Max number of lines a dict can have before being truncated. |
|
|
Set this to nil to prevent truncation." |
|
|
:type 'integer) |
|
|
|
|
|
(defun nrepl--expand-button (button) |
|
|
"Expand the text hidden under overlay BUTTON." |
|
|
(delete-overlay button)) |
|
|
|
|
|
(defun nrepl--pp (object &optional foreground) |
|
|
"Pretty print nREPL OBJECT, delimited using FOREGROUND." |
|
|
(if (not (and (listp object) |
|
|
(memq (car object) '(<- ---> dict)))) |
|
|
(progn (when (stringp object) |
|
|
(setq object (substring-no-properties object))) |
|
|
(pp object (current-buffer)) |
|
|
(unless (listp object) (insert "\n"))) |
|
|
(let* ((head (format "(%s" (car object)))) |
|
|
(cl-flet ((color (str) |
|
|
(propertize str 'face (append '(:weight ultra-bold) |
|
|
(when foreground `(:foreground ,foreground)))))) |
|
|
(insert (color head)) |
|
|
(let ((indent (+ 2 (- (current-column) (length head)))) |
|
|
(l (point))) |
|
|
(if (null (cdr object)) |
|
|
(insert ")\n") |
|
|
(insert " \n") |
|
|
(cl-loop for l on (cdr object) by #'cddr |
|
|
do (let ((str (format "%s%s " (make-string indent ? ) (car l)))) |
|
|
(insert str) |
|
|
(nrepl--pp (cadr l)))) |
|
|
(when (eq (car object) 'dict) |
|
|
(delete-char -1) |
|
|
(let ((truncate-lines t)) |
|
|
(when (and nrepl-dict-max-message-size |
|
|
(> (count-screen-lines l (point) t) |
|
|
nrepl-dict-max-message-size)) |
|
|
(make-button (1+ l) (point) |
|
|
'display "..." |
|
|
'action #'nrepl--expand-button |
|
|
'face 'link |
|
|
'help-echo "RET: Expand dict." |
|
|
'follow-link t)))) |
|
|
(insert (color ")\n")))))))) |
|
|
|
|
|
(defun nrepl-messages-buffer () |
|
|
"Return or create the buffer given by `nrepl-message-buffer-name'. |
|
|
The default buffer name is *nrepl-messages*." |
|
|
(or (get-buffer nrepl-message-buffer-name) |
|
|
(let ((buffer (get-buffer-create nrepl-message-buffer-name))) |
|
|
(with-current-buffer buffer |
|
|
(buffer-disable-undo) |
|
|
(nrepl-messages-mode) |
|
|
buffer)))) |
|
|
|
|
|
(defun nrepl-create-client-buffer-default (endpoint) |
|
|
"Create an nREPL client process buffer. |
|
|
ENDPOINT is a plist returned by `nrepl-connect'." |
|
|
(let ((buffer (generate-new-buffer |
|
|
(nrepl-connection-buffer-name default-directory |
|
|
(plist-get endpoint :host) |
|
|
(plist-get endpoint :port))))) |
|
|
(with-current-buffer buffer |
|
|
(buffer-disable-undo) |
|
|
(setq-local kill-buffer-query-functions nil)) |
|
|
buffer)) |
|
|
|
|
|
(provide 'nrepl-client) |
|
|
|
|
|
;;; nrepl-client.el ends here
|
|
|
|