Skip to content

Commit 34ba845

Browse files
committed
Merge pull request #808 from vitoshka/cider-connect
`cider-connect`: auto-detect ssh hosts and all running repls on local and remote hosts
2 parents f587820 + 57ac604 commit 34ba845

File tree

6 files changed

+162
-98
lines changed

6 files changed

+162
-98
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
## master (unreleased)
44

55
### New features
6-
6+
* `cider-connect` now asks for remote hosts defined in machine-wide `ssh`
7+
configuration files and automatically detects running instances of lein
8+
server, both on local and remote machines.
79
* New defcustom `cider-stacktrace-print-level`. Controls the `*print-level*` used when
810
pretty printing an exception cause's data. Defaults to 50.
911
* New interactive command `cider-undef`.

cider-interaction.el

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,9 @@ When invoked with a prefix ARG the command doesn't prompt for confirmation."
543543
Defaults to the current buffer.
544544
Return the tramp prefix, or nil if BUFFER is local."
545545
(let* ((buffer (or buffer (current-buffer)))
546-
(name (buffer-file-name buffer)))
546+
(name (or (buffer-file-name buffer)
547+
(with-current-buffer buffer
548+
default-directory))))
547549
(when (tramp-tramp-file-p name)
548550
(let ((vec (tramp-dissect-file-name name)))
549551
(tramp-make-tramp-file-name (tramp-file-name-method vec)

cider-util.el

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ to `fill-column'."
157157
"Join all STRINGS using SEPARATOR."
158158
(mapconcat 'identity strings separator))
159159

160+
(defun cider-join-with-val-prop (candidates &optional sep)
161+
"Each element EL in CANDIDATES join with SEP and set :val property to EL.
162+
Useful for `completing-read' when candidates are complex objects."
163+
(mapcar (lambda (el)
164+
(propertize (if (listp el)
165+
(cider-string-join el (or sep ":"))
166+
(format "%s" el))
167+
:val el))
168+
candidates))
169+
160170
(provide 'cider-util)
161171

162172
;;; cider-util.el ends here

cider.el

Lines changed: 101 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
(require 'cider-repl)
6666
(require 'cider-mode)
6767
(require 'cider-util)
68+
(require 'tramp-sh)
6869

6970
(defvar cider-version "0.8.0-snapshot"
7071
"Fallback version used when it cannot be extracted automatically.
@@ -91,14 +92,17 @@ This variable is used by `cider-connect'."
9192
:type 'list
9293
:group 'cider)
9394

94-
;; TODO: Implement a check for `cider-lein-command' over tramp
95-
(defun cider--lein-present-p ()
96-
"Check if `cider-lein-command' is on the `exec-path'.
95+
(defvar cider-ps-running-nrepls-command "ps u | grep leiningen"
96+
"Process snapshot command used in `cider-locate-running-nrepl-ports'.")
9797

98-
In case `default-directory' is non-local we assume the command is available."
99-
(or (file-remote-p default-directory)
100-
(executable-find cider-lein-command)
101-
(executable-find (concat cider-lein-command ".bat"))))
98+
(defvar cider-ps-running-nrepl-path-regexp-list
99+
'("\\(?:leiningen.original.pwd=\\)\\([^ ]+\\)"
100+
"\\(?:-classpath +:?\\(.+\\)/self-installs\\)")
101+
"Regexp list to extract project paths from output of `cider-ps-running-nrepls-command'.
102+
Sub-match 1 must be the project path.")
103+
104+
(defvar cider-host-history nil
105+
"Completion history for connection hosts.")
102106

103107
;;;###autoload
104108
(defun cider-version ()
@@ -129,50 +133,110 @@ start the server."
129133
(message "The %s executable (specified by `cider-lein-command') isn't on your exec-path"
130134
cider-lein-command)))
131135

132-
(defun cider-known-endpoint-candidates ()
133-
"Known endpoint candidates for establishing an nREPL connection.
134-
A default will be included consisting of `nrepl-default-host' and
135-
`nrepl-default-port'."
136-
(-distinct
137-
(mapcar (lambda (endpoint)
138-
(cider-string-join endpoint " "))
139-
(cons (list (nrepl-current-host) (nrepl-default-port))
140-
cider-known-endpoints))))
141-
142-
(defun cider-select-known-endpoint ()
143-
"Select an endpoint from known endpoints.
144-
The returned endpoint has the label removed."
145-
(let ((selected-endpoint (split-string
146-
(completing-read
147-
"Host: " (cider-known-endpoint-candidates)))))
148-
(if (= 3 (length selected-endpoint))
149-
(cdr selected-endpoint)
150-
selected-endpoint)))
151-
152136
;;;###autoload
153137
(defun cider-connect (host port)
154138
"Connect to an nREPL server identified by HOST and PORT.
155139
Create REPL buffer and start an nREPL client connection."
156-
(interactive (let ((known-endpoint (when cider-known-endpoints
157-
(cider-select-known-endpoint))))
158-
(list (or (car known-endpoint)
159-
(read-string "Host: " (nrepl-current-host) nil (nrepl-current-host)))
160-
(string-to-number (let ((port (or (cadr known-endpoint) (nrepl-default-port))))
161-
(read-string "Port: " port nil port))))))
140+
(interactive (cider-select-endpoint))
162141
(setq cider-current-clojure-buffer (current-buffer))
163142
(when (nrepl-check-for-repl-buffer `(,host ,port) nil)
164-
(nrepl-start-client-process default-directory host port t)))
143+
(nrepl-start-client-process host port t)))
144+
145+
(defun cider-select-endpoint ()
146+
"Interactively select the host and port to connect to."
147+
(let* ((ssh-hosts (cider--ssh-hosts))
148+
(hosts (-distinct (append (when cider-host-history
149+
(list (car cider-host-history )))
150+
(list (list (nrepl-current-host)))
151+
cider-known-endpoints
152+
ssh-hosts
153+
(when (file-remote-p default-directory)
154+
;; add localhost even in remote buffers
155+
(list (list "localhost"))))))
156+
(sel-host (cider--completing-read-host hosts))
157+
(host (car sel-host))
158+
(local-p (or (nrepl-local-host-p host)
159+
(not (assoc-string host ssh-hosts))))
160+
;; Each lein-port is a list of the form (dir port)
161+
(lein-ports (if local-p
162+
(let ((default-directory (if (file-remote-p default-directory)
163+
"~/"
164+
default-directory)))
165+
(cider-locate-running-nrepl-ports))
166+
(let ((vec (vector "ssh" nil host "" nil)))
167+
(tramp-maybe-open-connection vec)
168+
(with-current-buffer (tramp-get-connection-buffer vec)
169+
(cider-locate-running-nrepl-ports)))))
170+
(ports (append (cdr sel-host) lein-ports))
171+
(port (cider--completing-read-port host ports)))
172+
(setq cider-host-history (cons sel-host (delete sel-host cider-host-history)))
173+
(list host port)))
174+
175+
(defun cider--ssh-hosts ()
176+
"Retrieve all ssh host from local configuration files."
177+
(-map (lambda (s) (list (replace-regexp-in-string ":$" "" s)))
178+
(let ((tramp-completion-mode t))
179+
(tramp-completion-handle-file-name-all-completions "" "/ssh:"))))
180+
181+
(defun cider--completing-read-host (hosts)
182+
"Interactively select host from HOSTS.
183+
Each element in HOSTS is one of: (host), (host port) or (label host port).
184+
Return a list of the form (HOST PORT), where PORT can be nil."
185+
(let* ((sel-host (completing-read "Host: " (cider-join-with-val-prop hosts)))
186+
(host (or (get-text-property 1 :val sel-host) (list sel-host))))
187+
;; remove the label
188+
(if (= 3 (length host)) (cdr host) host)))
189+
190+
(defun cider--completing-read-port (host ports)
191+
"Interactively select port for HOST from PORTS."
192+
(let* ((sel-port (completing-read (format "Port for %s: " host)
193+
(cider-join-with-val-prop ports)))
194+
(port (or (get-text-property 1 :val sel-port) sel-port)))
195+
(if (listp port) (second port) port)))
196+
197+
(defun cider-locate-running-nrepl-ports ()
198+
"Locate ports of running nREPL servers.
199+
Return a list of list of the form (project-dir port)."
200+
(let ((paths (cider--get-running-nrepl-paths)))
201+
(delq nil
202+
(mapcar (lambda (f)
203+
(-when-let (port-file (or (cider--file-path (concat f "/.nrepl-port"))
204+
(cider--file-path (concat f "/repl-port"))))
205+
(with-temp-buffer
206+
(insert-file-contents port-file)
207+
(list (file-name-nondirectory f) (buffer-string)))))
208+
paths))))
209+
210+
(defun cider--get-running-nrepl-paths ()
211+
"Retrieve project paths of running nREPL servers.
212+
use `cider-ps-running-nrepls-command' and `cider-ps-running-nrepl-path-regexp-list'."
213+
(let (paths)
214+
(with-temp-buffer
215+
(insert (shell-command-to-string cider-ps-running-nrepls-command))
216+
(dolist (regexp cider-ps-running-nrepl-path-regexp-list)
217+
(goto-char 1)
218+
(while (re-search-forward regexp nil t)
219+
(setq paths (cons (match-string 1) paths)))))
220+
(-distinct paths)))
165221

166-
(define-obsolete-function-alias
167-
'cider
168-
'cider-connect)
222+
;; TODO: Implement a check for `cider-lein-command' over tramp
223+
(defun cider--lein-present-p ()
224+
"Check if `cider-lein-command' is on the `exec-path'.
225+
226+
In case `default-directory' is non-local we assume the command is available."
227+
(or (file-remote-p default-directory)
228+
(executable-find cider-lein-command)
229+
(executable-find (concat cider-lein-command ".bat"))))
169230

170231
;;;###autoload
171232
(eval-after-load 'clojure-mode
172233
'(progn
173234
(define-key clojure-mode-map (kbd "C-c M-j") 'cider-jack-in)
174235
(define-key clojure-mode-map (kbd "C-c M-c") 'cider-connect)))
175236

237+
238+
(define-obsolete-function-alias 'cider 'cider-connect)
239+
176240
(provide 'cider)
177241

178242
;;; cider.el ends here

nrepl-client.el

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ Bind the value of the provided KEYS and execute BODY."
251251
(file-name-directory file-name))
252252
list-buffers-directory)))
253253

254+
(defun nrepl-local-host-p (host)
255+
"Return t if HOST is local."
256+
(string-match-p tramp-local-host-regexp host))
257+
254258
(defun nrepl-project-directory-for (dir-name)
255259
"Return the project directory for the specified DIR-NAME."
256260
(when dir-name
@@ -587,22 +591,20 @@ process buffer and run the hook `nrepl-disconnected-hook'."
587591
;; `nrepl-start-client-process' is called from `nrepl-server-filter'. It
588592
;; starts the client process described by `nrepl-client-filter' and
589593
;; `nrepl-client-sentinel'.
590-
(defun nrepl-start-client-process (&optional directory host port replp server-proc)
591-
"Create new client process identified by DIRECTORY, HOST and PORT.
592-
If DIRECTORY is nil, use `default-directory'. If eitehr HOST or PORT are
593-
nil, pick them from the value returned by `nrepl-connection-endpoint'. If
594-
REPLP is non-nil create a client connection which is associated with a repl
595-
buffer. When non-nil, SERVER-PROC must be a running nrepl server process
596-
within Emacs. Return the newly created client connection process."
597-
(let* ((endpoint (unless (and host port)
598-
(nrepl-connection-endpoint directory port)))
599-
(directory (or directory default-directory))
600-
(host (or host (plist-get endpoint :hostname)))
601-
(port (or port (plist-get endpoint :port)))
594+
(defun nrepl-start-client-process (&optional host port replp server-proc)
595+
"Create new client process identified by HOST and PORT.
596+
If eitehr HOST or PORT are nil, pick them from the value returned by
597+
`nrepl-connection-endpoint'. If REPLP is non-nil create a client
598+
connection which is associated with a repl buffer. When non-nil,
599+
SERVER-PROC must be a running nrepl server process within Emacs. Return
600+
the newly created client connection process."
601+
(let* ((endpoint (nrepl-connection-endpoint host port))
602+
(host (plist-get endpoint :host))
603+
(port (plist-get endpoint :port))
602604
(server-buf (and server-proc (process-buffer server-proc)))
603605
(client-buf (if replp
604-
(cider-repl-create directory host port)
605-
(nrepl-create-connection-buffer directory host port)))
606+
(cider-repl-create default-directory host port)
607+
(nrepl-create-connection-buffer default-directory host port)))
606608
(client-proc (open-network-stream "nrepl" client-buf host port))
607609
(tunnel-proc (plist-get endpoint :proc))
608610
(nrepl-connection-dispatch client-buf))
@@ -872,8 +874,7 @@ Return a newly created process."
872874
(let ((port (string-to-number (match-string 1 output))))
873875
(message (format "nREPL server started on %s" port))
874876
(with-current-buffer (process-buffer process)
875-
(let ((client-proc (nrepl-start-client-process
876-
default-directory nil port t process)))
877+
(let ((client-proc (nrepl-start-client-process nil port t process)))
877878
;; FIXME: Bad connection tracking system. There can be multiple client
878879
;; connections per server
879880
(setq nrepl-connection-buffer (buffer-name (process-buffer client-proc))))))))
@@ -898,36 +899,40 @@ Return a newly created process."
898899
(error "Leiningen 2.x is required by CIDER"))
899900
(t (error "Could not start nREPL server: %s" problem)))))
900901

901-
(defun nrepl-connection-endpoint (dir port)
902+
(defun nrepl-connection-endpoint (host port)
902903
"Return a connection endpoint.
903904
The returned endpoint is a `plist` of the form:
904905
905-
(:proc PROCESS :hostname \"hostname\" :port 1234)
906+
(:proc PROCESS :host \"hostname\" :port 1234)
906907
907-
If DIR is local :proc is nil, :hostname is \"localhost\" and :port is PORT.
908+
If HOST is local :proc will be nil, :host - \"localhost\" and :port - PORT.
908909
909-
If DIR is remote and `ssh' executable has been found, attempt to start an
910-
SSH tunnel and return it as :proc. If no `ssh' executable has been found,
911-
fall back to specifying a direct connection to the remote host."
912-
(if (file-remote-p dir)
910+
If HOST is nil and `default-directory' is remote, or if HOST is remote
911+
attempt to start an SSH tunnel and return it as :proc slot. If no `ssh'
912+
executable has been found, fall back to specifying a direct connection to
913+
the remote host."
914+
(let ((localp (if host
915+
(nrepl-local-host-p host)
916+
(not (file-remote-p default-directory)))))
917+
(if localp
918+
(list :host (or host "localhost") :port port :proc nil)
913919
(let ((ssh (executable-find "ssh")))
914-
(if ssh
915-
;; run cmd in a local shell
916-
(let* ((cmd (nrepl--ssh-tunnel-command ssh dir port))
917-
(proc (start-process-shell-command
918-
"nrepl-tunnel"
919-
(nrepl-tunnel-buffer-name)
920-
cmd)))
921-
(process-put proc :waiting-for-port t)
922-
(set-process-filter proc (nrepl--ssh-tunnel-filter port))
923-
(while (and (process-live-p proc)
924-
(process-get proc :waiting-for-port))
925-
(accept-process-output nil 0.005))
926-
(unless (process-live-p proc)
927-
(message "SSH port forwarding failed"))
928-
(list :hostname "localhost" :port port :proc proc))
929-
(list :hostname tramp-current-host :port port :proc nil)))
930-
(list :hostname "localhost" :port port :proc nil)))
920+
(if ssh
921+
;; create a tunnel
922+
(let* ((remote-dir (if host (format "/ssh:%s:" host) default-directory))
923+
(cmd (nrepl--ssh-tunnel-command ssh remote-dir port))
924+
(proc (start-process-shell-command
925+
"nrepl-tunnel" (nrepl-tunnel-buffer-name) cmd)))
926+
(process-put proc :waiting-for-port t)
927+
(set-process-filter proc (nrepl--ssh-tunnel-filter port))
928+
(while (and (process-live-p proc)
929+
(process-get proc :waiting-for-port))
930+
(accept-process-output nil 0.005))
931+
(unless (process-live-p proc)
932+
(message "SSH port forwarding failed"))
933+
(list :host "localhost" :port port :proc proc))
934+
;; try a direct connection
935+
(list :host tramp-current-host :port port :proc nil))))))
931936

932937
(defun nrepl--ssh-tunnel-command (ssh dir port)
933938
"Command string to open SSH tunnel to the host associated with DIR's PORT."

test/cider-tests.el

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -455,25 +455,6 @@
455455
(dolist (buf (list b1 b2 b3 b1-repl b2-repl b3-repl))
456456
(kill-buffer buf)))))
457457

458-
(ert-deftest test-cider-known-endpoint-candidates ()
459-
(let ((cider-known-endpoints '(("label" "host" "port"))))
460-
(noflet ((nrepl-current-host () "current-host")
461-
(nrepl-default-port () "current-port"))
462-
(should (equal '("current-host current-port" "label host port")
463-
(cider-known-endpoint-candidates))))))
464-
465-
(ert-deftest test-cider-known-endpoint-candidates-remove-duplicates ()
466-
(let ((cider-known-endpoints '(("label" "host" "port") ("label" "host" "port"))))
467-
(noflet ((nrepl-current-host () "current-host")
468-
(nrepl-default-port () "current-port"))
469-
(should (equal '("current-host current-port" "label host port")
470-
(cider-known-endpoint-candidates))))))
471-
472-
(ert-deftest test-cider-select-known-endpoint-remove-label ()
473-
(noflet ((cider-known-endpoint-candidates () '())
474-
(completing-read (dontcare dontcare) "label host port"))
475-
(should (equal '("host" "port") (cider-select-known-endpoint)))))
476-
477458
(ert-deftest test-cider-change-buffers-designation ()
478459
(with-temp-buffer
479460
(let ((server-buffer (current-buffer)))

0 commit comments

Comments
 (0)