Permalink
Cannot retrieve contributors at this time
Fetching contributors…

;;; nrepl.el --- Client for Clojure nREPL | |
;; Copyright © 2012 Tim King, Phil Hagelberg | |
;; | |
;; Author: Tim King <kingtim@gmail.com> | |
;; Phil Hagelberg <technomancy@gmail.com> | |
;; URL: http://www.github.com/kingtim/nrepl.el | |
;; Version: 0.1.4 | |
;; Keywords: languages, clojure, nrepl | |
;; Package-Requires: ((clojure-mode "1.11")) | |
;; 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 elisp client to connect to Clojure nREPL servers. | |
;;; Installation: | |
;; Available as a package in marmalade-repo.org. | |
;; (add-to-list 'package-archives | |
;; '("marmalade" . "http://marmalade-repo.org/packages/")) | |
;; M-x package-install nrepl | |
;;; Usage: | |
;; M-x nrepl-jack-in | |
;;; Code: | |
(require 'clojure-mode) | |
(require 'thingatpt) | |
(require 'etags) | |
(require 'arc-mode) | |
(require 'ansi-color) | |
(require 'eldoc) | |
(eval-when-compile | |
(require 'cl)) | |
(defgroup nrepl nil | |
"Interaction with the Clojure nREPL Server." | |
:prefix "nrepl-" | |
:group 'applications) | |
(defcustom nrepl-connected-hook nil | |
"List of functions to call when connecting to the nREPL server." | |
:type 'hook | |
:group 'nrepl) | |
(defvar nrepl-version "0.1.4-preview" | |
"The current nrepl version.") | |
(defface nrepl-prompt-face | |
'((t (:inherit font-lock-keyword-face))) | |
"Face for the prompt in the nREPL client." | |
:group 'nrepl) | |
(defface nrepl-output-face | |
'((t (:inherit font-lock-string-face))) | |
"Face for output in the nREPL client." | |
:group 'nrepl) | |
(defface nrepl-error-face | |
'((t (:inherit font-lock-string-face))) | |
"Face for errors in the nREPL client." | |
:group 'nrepl) | |
(defface nrepl-input-face | |
'((t (:bold t))) | |
"Face for previous input in the nREPL client." | |
:group 'nrepl) | |
(defface nrepl-result-face | |
'((t ())) | |
"Face for the result of an evaluation in the nREPL client." | |
:group 'nrepl) | |
(defmacro nrepl-propertize-region (props &rest body) | |
"Execute BODY and add PROPS to all the text it inserts. | |
More precisely, PROPS are added to the region between the point's | |
positions before and after executing BODY." | |
(let ((start (make-symbol "start-pos"))) | |
`(let ((,start (point))) | |
(prog1 (progn ,@body) | |
(add-text-properties ,start (point) ,props))))) | |
(put 'nrepl-propertize-region 'lisp-indent-function 1) | |
;; buffer local declarations | |
(defvar nrepl-session nil | |
"Current nREPL session id.") | |
(defvar nrepl-input-start-mark) | |
(defvar nrepl-prompt-start-mark) | |
(defvar nrepl-request-counter 0 | |
"Continuation serial number counter.") | |
(defvar nrepl-old-input-counter 0 | |
"Counter used to generate unique `nrepl-old-input' properties. | |
This property value must be unique to avoid having adjacent inputs be | |
joined together.") | |
(defvar nrepl-requests (make-hash-table :test 'equal)) | |
(defvar nrepl-buffer-ns "user" | |
"Current clojure namespace of this buffer.") | |
(defvar nrepl-input-history '() | |
"History list of strings read from the nREPL buffer.") | |
(defvar nrepl-input-history-index 0 | |
"Current position in the history list.") | |
(defvar nrepl-input-history-items-added 0 | |
"Variable counting the items added in the current session.") | |
(defvar nrepl-output-start nil | |
"Marker for the start of output.") | |
(defvar nrepl-output-end | |
"Marker for the end of output.") | |
(defvar nrepl-sync-response nil | |
"Result of the last sync request.") | |
(defvar nrepl-err-handler 'nrepl-default-err-handler | |
"Evaluation error handler") | |
(defvar nrepl-extra-eldoc-commands '("nrepl-complete" "yas/expand") | |
"Extra commands to be added to eldoc's safe commands list.") | |
(defcustom nrepl-popup-stacktraces t | |
"Non-nil means pop-up error stacktraces. | |
Nil means do not, useful when in repl" | |
:type 'boolean | |
:group 'nrepl) | |
(defun nrepl-make-variables-buffer-local (&rest variables) | |
(mapcar #'make-variable-buffer-local variables)) | |
(nrepl-make-variables-buffer-local | |
'nrepl-session | |
'nrepl-input-start-mark | |
'nrepl-prompt-start-mark | |
'nrepl-request-counter | |
'nrepl-requests | |
'nrepl-old-input-counter | |
'nrepl-buffer-ns | |
'nrepl-input-history | |
'nrepl-input-history-items-added | |
'nrepl-current-input-history-index | |
'nrepl-output-start | |
'nrepl-output-end | |
'nrepl-sync-response) | |
(defun nrepl-reset-markers () | |
(dolist (markname '(nrepl-output-start | |
nrepl-output-end | |
nrepl-prompt-start-mark | |
nrepl-input-start-mark)) | |
(set markname (make-marker)) | |
(set-marker (symbol-value markname) (point)))) | |
;;; Bencode | |
;;; Adapted from http://www.emacswiki.org/emacs-en/bencode.el | |
;;; and modified to work with utf-8 | |
(defun nrepl-bdecode-buffer () | |
"Decode a bencoded string in the current buffer starting at point." | |
(cond ((looking-at "i\\([0-9]+\\)e") | |
(goto-char (match-end 0)) | |
(string-to-number (match-string 1))) | |
((looking-at "\\([0-9]+\\):") | |
(goto-char (match-end 0)) | |
(let ((start (point)) | |
(end (byte-to-position (+ (position-bytes (point)) (string-to-number (match-string 1)))))) | |
(goto-char end) | |
(buffer-substring-no-properties start end))) | |
((looking-at "l") | |
(goto-char (match-end 0)) | |
(let (result item) | |
(while (setq item (nrepl-bdecode-buffer)) | |
(setq result (cons item result))) | |
(nreverse result))) | |
((looking-at "d") | |
(goto-char (match-end 0)) | |
(let (dict key item) | |
(while (setq item (nrepl-bdecode-buffer)) | |
(if key | |
(setq dict (cons (cons key item) dict) | |
key nil) | |
(unless (stringp item) | |
(error "Dictionary keys have to be strings: %s" item)) | |
(setq key item))) | |
(cons 'dict (nreverse dict)))) | |
((looking-at "e") | |
(goto-char (match-end 0)) | |
nil) | |
(t | |
(error "Cannot decode object: %d" (point))))) | |
(defun nrepl-decode (str) | |
(with-temp-buffer | |
(save-excursion | |
(insert str)) | |
(let ((result '())) | |
(while (not (eobp)) | |
(setq result (cons (nrepl-bdecode-buffer) result))) | |
(nreverse result)))) | |
(defun nrepl-netstring (string) | |
(let ((size (string-bytes string))) | |
(format "%s:%s" size string))) | |
(defun nrepl-bencode (message) | |
(concat "d" (apply 'concat (mapcar 'nrepl-netstring message)) "e")) | |
(defun nrepl-eval-region (start end) | |
"Evaluate region." | |
(interactive "r") | |
(nrepl-interactive-eval (buffer-substring-no-properties start end))) | |
(defun nrepl-eval-buffer () | |
"Evaluate the current buffer." | |
(interactive) | |
(nrepl-eval-region (point-min) (point-max))) | |
(defun nrepl-expression-at-point () | |
"Return the text of the expr at point." | |
(apply #'buffer-substring-no-properties | |
(nrepl-region-for-expression-at-point))) | |
(defun nrepl-region-for-expression-at-point () | |
"Return the start and end position of defun at point." | |
(save-excursion | |
(save-match-data | |
(end-of-defun) | |
(let ((end (point))) | |
(beginning-of-defun) | |
(list (point) end))))) | |
(defun nrepl-eval-expression-at-point (&optional prefix) | |
"Evaluate the current toplevel form." | |
(interactive "P") | |
(let ((form (nrepl-expression-at-point))) | |
(if prefix | |
(nrepl-interactive-eval-print form) | |
(nrepl-interactive-eval form)))) | |
(defun nrepl-eval-ns-form () | |
"Evaluate the current buffer's namespace form." | |
(interactive) | |
(when (clojure-find-ns) | |
(save-excursion | |
(goto-char (match-beginning 0)) | |
(nrepl-eval-expression-at-point)))) | |
(defun nrepl-last-expression () | |
(buffer-substring-no-properties | |
(save-excursion (backward-sexp) (point)) | |
(point))) | |
(defun nrepl-find-resource (resource) | |
(cond ((string-match "^file:\\(.+\\)" resource) | |
(find-file (match-string 1 resource))) | |
((string-match "^\\(jar\\|zip\\):file:\\(.+\\)!/\\(.+\\)" resource) | |
(let* ((jar (match-string 2 resource)) | |
(path (match-string 3 resource)) | |
(buffer-already-open (get-buffer (file-name-nondirectory jar)))) | |
(find-file jar) | |
(goto-char (point-min)) | |
(search-forward path) | |
(let ((opened-buffer (current-buffer))) | |
(archive-extract) | |
(when (not buffer-already-open) | |
(kill-buffer opened-buffer))))) | |
(:else (error "Unknown resource path %s" resource)))) | |
(defun nrepl-jump-to-def-for (location) | |
;; ugh; elisp destructuring doesn't work for vectors | |
(let ((resource (aref location 0)) | |
(path (aref location 1)) | |
(line (aref location 2))) | |
(if (and path (file-exists-p path)) | |
(find-file path) | |
(nrepl-find-resource resource)) | |
(goto-char (point-min)) | |
(forward-line (1- line)) | |
(search-forward-regexp "(def[^\s]* +" nil t))) | |
(defun nrepl-jump-to-def-handler (buffer) | |
;; TODO: got to be a simpler way to do this | |
(nrepl-make-response-handler buffer | |
(lambda (buffer value) | |
(with-current-buffer buffer | |
(ring-insert find-tag-marker-ring (point-marker))) | |
(nrepl-jump-to-def-for | |
(car (read-from-string value)))) | |
(lambda (buffer out) (message out)) | |
(lambda (buffer err) (message err)) | |
nil)) | |
(defun nrepl-jump-to-def (var) | |
"Jump to the definition of the var at point." | |
(let ((form (format "((clojure.core/juxt | |
(comp clojure.core/str clojure.java.io/resource :file) | |
(comp clojure.core/str clojure.java.io/file :file) :line) | |
(clojure.core/meta (clojure.core/resolve '%s)))" | |
var))) | |
(nrepl-send-string form | |
(nrepl-jump-to-def-handler (current-buffer)) | |
nrepl-buffer-ns))) | |
(defun nrepl-jump (query) | |
(interactive "P") | |
(nrepl-read-symbol-name "Symbol: " 'nrepl-jump-to-def query)) | |
(defalias 'nrepl-jump-back 'pop-tag-mark) | |
(defvar nrepl-completion-fn 'nrepl-completion-complete-core-fn) | |
(defun nrepl-completion-complete-core-fn (str) | |
"Return a list of completions using complete.core/completions." | |
(let ((strlst (plist-get | |
(nrepl-send-string-sync | |
(format "(complete.core/completions \"%s\" *ns*)" str) | |
nrepl-buffer-ns) | |
:value))) | |
(when strlst | |
(car (read-from-string strlst))))) | |
(defun nrepl-complete-at-point () | |
(let ((sap (symbol-at-point))) | |
(when (and sap (not (in-string-p))) | |
(let ((bounds (bounds-of-thing-at-point 'symbol))) | |
(list (car bounds) (cdr bounds) | |
(completion-table-dynamic nrepl-completion-fn)))))) | |
(defun nrepl-eldoc-format-thing (thing) | |
(propertize thing 'face 'font-lock-function-name-face)) | |
(defun nrepl-eldoc-format-arglist (arglist) | |
;; TODO: find out which arglist variant is in use and which argument | |
;; is currently under point. Highlight that argument | |
;; for now: | |
arglist) | |
(defun nrepl-eldoc-handler (buffer the-thing) | |
(lexical-let ((thing the-thing)) | |
(nrepl-make-response-handler | |
buffer | |
(lambda (buffer value) | |
(when (not (string-equal value "nil")) | |
(message (format "%s: %s" | |
(nrepl-eldoc-format-thing thing) | |
(nrepl-eldoc-format-arglist value))))) | |
nil nil nil))) | |
(defun nrepl-eldoc () | |
"Backend function for eldoc to show argument list in the echo area." | |
(let* ((thing (nrepl-operator-before-point)) | |
(form (format "(try | |
(:arglists | |
(clojure.core/meta | |
(clojure.core/resolve | |
(clojure.core/read-string \"%s\")))) | |
(catch Throwable t nil))" thing))) | |
(when thing | |
(nrepl-send-string form | |
(nrepl-eldoc-handler (current-buffer) | |
thing) | |
nrepl-buffer-ns)))) | |
(defun nrepl-eldoc-enable-in-current-buffer () | |
(make-local-variable 'eldoc-documentation-function) | |
(setq eldoc-documentation-function 'nrepl-eldoc) | |
(apply 'eldoc-add-command nrepl-extra-eldoc-commands) | |
(turn-on-eldoc-mode)) | |
;;; Response handlers | |
(defmacro nrepl-dbind-response (response keys &rest body) | |
"Destructure an nREPL response dict." | |
`(let ,(loop for key in keys | |
collect `(,key (cdr (assoc ,(format "%s" key) ,response)))) | |
,@body)) | |
(put 'nrepl-dbind-response 'lisp-indent-function 2) | |
(defun nrepl-make-response-handler (buffer value-handler stdout-handler stderr-handler done-handler) | |
(lexical-let ((buffer buffer) | |
(value-handler value-handler) | |
(stdout-handler stdout-handler) | |
(stderr-handler stderr-handler) | |
(done-handler done-handler)) | |
(lambda (response) | |
(nrepl-dbind-response response (value ns out err status id ex root-ex) | |
(cond (value | |
(with-current-buffer buffer | |
(if ns | |
(setq nrepl-buffer-ns ns))) | |
(if value-handler | |
(funcall value-handler buffer value))) | |
(out | |
(if stdout-handler | |
(funcall stdout-handler buffer out))) | |
(err | |
(if stderr-handler | |
(funcall stderr-handler buffer err))) | |
(status | |
(if (member "interrupted" status) | |
(message "Evaluation interrupted.")) | |
(if (member "eval-error" status) | |
(funcall nrepl-err-handler buffer ex root-ex)) | |
(if (member "namespace-not-found" status) | |
(message "Namespace not found.")) | |
(if (member "need-input" status) | |
(nrepl-need-input buffer)) | |
(if (member "done" status) | |
(progn (remhash id nrepl-requests) | |
(if done-handler | |
(funcall done-handler buffer)))))))))) | |
(defun nrepl-stdin-handler (buffer) | |
(nrepl-make-response-handler buffer | |
(lambda (buffer value) | |
(nrepl-emit-result buffer value t)) | |
(lambda (buffer out) | |
(nrepl-emit-output buffer out t)) | |
(lambda (buffer err) | |
(nrepl-emit-output buffer err t)) | |
nil)) | |
(defun nrepl-handler (buffer) | |
(nrepl-make-response-handler buffer | |
(lambda (buffer value) | |
(nrepl-emit-result buffer value t)) | |
(lambda (buffer out) | |
(nrepl-emit-output buffer out t)) | |
(lambda (buffer err) | |
(nrepl-emit-output buffer err t)) | |
(lambda (buffer) | |
(nrepl-emit-prompt buffer)))) | |
(defun nrepl-interactive-eval-handler (buffer) | |
(nrepl-make-response-handler buffer | |
(lambda (buffer value) | |
(message (format "%s" value))) | |
(lambda (buffer value) | |
(nrepl-emit-interactive-output value)) | |
(lambda (buffer err) | |
(message (format "%s" err))) | |
'())) | |
(defun nrepl-interactive-eval-print-handler (buffer) | |
(nrepl-make-response-handler buffer | |
(lambda (buffer value) | |
(with-current-buffer buffer | |
(insert (format "%s" value)))) | |
'() | |
(lambda (buffer err) | |
(message (format "%s" err))) | |
'())) | |
(defun nrepl-popup-eval-print-handler (buffer) | |
(nrepl-make-response-handler buffer | |
(lambda (buffer str) | |
(nrepl-emit-into-popup-buffer buffer str)) | |
'() | |
(lambda (buffer str) | |
(nrepl-emit-into-popup-buffer buffer str)) | |
'())) | |
(defun nrepl-popup-eval-out-handler (buffer) | |
(nrepl-make-response-handler buffer | |
'() | |
(lambda (buffer str) | |
(nrepl-emit-into-popup-buffer buffer str)) | |
(lambda (buffer str) | |
(nrepl-emit-into-popup-buffer buffer str)) | |
'())) | |
(defun nrepl-default-err-handler (buffer ex root-ex) | |
;; TODO: use pst+ here for colorization. currently breaks bencode. | |
;; TODO: use ex and root-ex as fallback values to display when pst/print-stack-trace-not-found | |
(if (or nrepl-popup-stacktraces | |
(not (eq 'nrepl-mode | |
(cdr (assq 'major-mode | |
(buffer-local-variables buffer)))))) | |
(with-current-buffer buffer | |
(nrepl-send-string "(if-let [pst+ (clojure.core/resolve 'clj-stacktrace.repl/pst+)] | |
(pst+ *e) (clojure.stacktrace/print-stack-trace *e))" | |
(nrepl-make-response-handler | |
(nrepl-popup-buffer "*nREPL error*" t) | |
nil | |
'nrepl-emit-into-color-buffer nil nil))) | |
;; TODO: maybe put the stacktrace in a tmp buffer somewhere that the user | |
;; can pull up with a hotkey only when interested in seeing it? | |
)) | |
(defun nrepl-need-input (buffer) | |
(with-current-buffer buffer | |
(nrepl-send-stdin (concat (read-from-minibuffer "Stdin: ") "\n") | |
(nrepl-stdin-handler buffer)))) | |
;;;; Popup buffers | |
(defvar nrepl-popup-restore-data nil | |
"Data needed when closing popup windows. | |
This is used as buffer local variable. | |
The format is (POPUP-WINDOW SELECTED-WINDOW OLD-BUFFER). | |
POPUP-WINDOW is the window used to display the temp buffer. | |
That window may have been reused or freshly created. | |
SELECTED-WINDOW is the window that was selected before displaying | |
the popup buffer. | |
OLD-BUFFER is the buffer that was previously displayed in POPUP-WINDOW. | |
OLD-BUFFER is nil if POPUP-WINDOW was newly created.") | |
(define-minor-mode nrepl-popup-buffer-mode | |
"Mode for nrepl popup buffers" | |
nil | |
(" nREPL-tmp") | |
'(("q" . nrepl-popup-buffer-quit-function))) | |
(make-variable-buffer-local | |
(defvar nrepl-popup-buffer-quit-function 'nrepl-popup-buffer-quit | |
"The function that is used to quit a temporary popup buffer.")) | |
(defun nrepl-popup-buffer-quit-function (&optional kill-buffer-p) | |
"Wrapper to invoke the value of `nrepl-popup-buffer-quit-function'." | |
(interactive) | |
(funcall nrepl-popup-buffer-quit-function kill-buffer-p)) | |
(defun nrepl-popup-buffer (name select) | |
(with-current-buffer (nrepl-make-popup-buffer name) | |
(setq buffer-read-only t) | |
(set-window-point (nrepl-display-popup-buffer select) (point)) | |
(current-buffer))) | |
(defun nrepl-display-popup-buffer (select) | |
"Display the current buffer. | |
Save the selected-window in a buffer-local variable, so that we | |
can restore it later." | |
(let ((selected-window (selected-window)) | |
(old-windows)) | |
(walk-windows (lambda (w) (push (cons w (window-buffer w)) old-windows)) | |
nil t) | |
(let ((new-window (display-buffer (current-buffer)))) | |
(unless nrepl-popup-restore-data | |
(set (make-local-variable 'nrepl-popup-restore-data) | |
(list new-window | |
selected-window | |
(cdr (find new-window old-windows :key #'car))))) | |
(when select | |
(select-window new-window)) | |
new-window))) | |
(defun nrepl-close-popup-window () | |
(when nrepl-popup-restore-data | |
(destructuring-bind (popup-window selected-window old-buffer) | |
nrepl-popup-restore-data | |
(bury-buffer) | |
(when (eq popup-window (selected-window)) | |
(cond ((and (not old-buffer) (not (one-window-p))) | |
(delete-window popup-window)) | |
((and old-buffer (buffer-live-p old-buffer)) | |
(set-window-buffer popup-window old-buffer)))) | |
(when (window-live-p selected-window) | |
(select-window selected-window)))) | |
(kill-local-variable 'nrepl-popup-restore-data)) | |
(defun nrepl-popup-buffer-quit (&optional kill-buffer-p) | |
"Get rid of the current (temp) buffer without asking. | |
Restore the window configuration unless it was changed since we | |
last activated the buffer." | |
(interactive) | |
(let ((buffer (current-buffer))) | |
(nrepl-close-popup-window) | |
(when kill-buffer-p | |
(kill-buffer buffer)))) | |
(defun nrepl-make-popup-buffer (name) | |
"Create a temporary buffer called NAME." | |
(with-current-buffer (get-buffer-create name) | |
(kill-all-local-variables) | |
(setq buffer-read-only nil) | |
(erase-buffer) | |
(set-syntax-table clojure-mode-syntax-table) | |
(nrepl-popup-buffer-mode 1) | |
(current-buffer))) | |
(defun nrepl-emit-into-popup-buffer (buffer value) | |
(with-current-buffer buffer | |
(let ((inhibit-read-only t) | |
(buffer-undo-list t)) | |
(insert (format "%s" value)) | |
(indent-sexp) | |
(font-lock-fontify-buffer)))) | |
(defun nrepl-emit-into-color-buffer (buffer value) | |
(with-current-buffer buffer | |
(let ((inhibit-read-only t) | |
(buffer-undo-list t)) | |
(goto-char (point-max)) | |
(insert (format "%s" value)) | |
(ansi-color-apply-on-region (point-min) (point-max))) | |
(goto-char (point-min)))) | |
;;;; Macroexpansion | |
(define-minor-mode nrepl-macroexpansion-minor-mode | |
"Mode for nrepl macroexpansion buffers" | |
nil | |
(" ") | |
'(("g" . nrepl-macroexpand-1-last-expression))) | |
(defun nrepl-macroexpand-expr (macroexpand expr pprint-p &optional buffer) | |
"Evaluate the expression preceding point and print the result | |
into the special buffer. Prefix argument forces pretty-printed output." | |
(interactive "P") | |
(let* ((ns nrepl-buffer-ns) | |
(form (format | |
(if pprint-p | |
"(clojure.pprint/pprint (%s '%s))" | |
"(%s '%s)") macroexpand expr)) | |
(macroexpansion-buffer (or buffer (nrepl-initialize-macroexpansion-buffer))) | |
(handler (if pprint-p | |
#'nrepl-popup-eval-out-handler | |
#'nrepl-popup-eval-print-handler))) | |
(nrepl-send-string form | |
(funcall handler macroexpansion-buffer) | |
ns))) | |
(defun nrepl-macroexpand-last-expression (&optional prefix) | |
"Invoke 'macroexpand' on the expression preceding point and display the result | |
in a macroexpansion buffer. Prefix argument forces pretty-printed output." | |
(interactive "P") | |
(nrepl-macroexpand-expr 'macroexpand (nrepl-last-expression) prefix)) | |
(defun nrepl-macroexpand-1-last-expression (&optional prefix) | |
"Invoke 'macroexpand-1' on the expression preceding point and display the result | |
in a macroexpansion buffer. Prefix argument forces pretty-printed output." | |
(interactive "P") | |
(nrepl-macroexpand-expr 'macroexpand-1 (nrepl-last-expression) prefix)) | |
(defun nrepl-macroexpand-all-last-expression (&optional prefix) | |
"Invoke 'clojure.walk/macroexpand-all' on the expression preceding point and display the result | |
in a macroexpansion buffer. Prefix argument forces pretty-printed output." | |
(interactive "P") | |
(nrepl-macroexpand-expr 'clojure.walk/macroexpand-all (nrepl-last-expression) prefix)) | |
(defun nrepl-initialize-macroexpansion-buffer (&optional buffer) | |
(pop-to-buffer (or buffer (nrepl-create-macroexpansion-buffer)))) | |
(defun nrepl-create-macroexpansion-buffer () | |
(with-current-buffer (nrepl-popup-buffer "*nREPL Macroexpansion*" t) | |
(nrepl-macroexpansion-minor-mode 1) | |
(current-buffer))) | |
(defun nrepl-popup-eval-print (form) | |
"Evaluate the given form and print value in current buffer." | |
(let ((buffer (current-buffer))) | |
(nrepl-send-string form | |
(nrepl-popup-eval-print-handler buffer) | |
nrepl-buffer-ns))) | |
(defun nrepl-interactive-eval-print (form) | |
"Evaluate the given form and print value in current buffer." | |
(let ((buffer (current-buffer))) | |
(nrepl-send-string form | |
(nrepl-interactive-eval-print-handler buffer) | |
nrepl-buffer-ns))) | |
(defun nrepl-interactive-eval (form) | |
"Evaluate the given form and print value in minibuffer." | |
(let ((buffer (current-buffer))) | |
(nrepl-send-string form | |
(nrepl-interactive-eval-handler buffer) | |
nrepl-buffer-ns))) | |
(defun nrepl-eval-last-expression (&optional prefix) | |
"Evaluate the expression preceding point." | |
(interactive "P") | |
(if prefix | |
(nrepl-interactive-eval-print (nrepl-last-expression)) | |
(nrepl-interactive-eval (nrepl-last-expression)))) | |
;;;;; History | |
(defun nrepl-add-to-input-history (string) | |
"Add STRING to the input history. | |
Empty strings and duplicates are ignored." | |
(unless (or (equal string "") | |
(equal string (car nrepl-input-history))) | |
(push string nrepl-input-history) | |
(incf nrepl-input-history-items-added))) | |
(defun nrepl-delete-current-input () | |
"Delete all text after the prompt." | |
(interactive) | |
(delete-region nrepl-input-start-mark (point-max))) | |
(defun nrepl-replace-input (string) | |
(nrepl-delete-current-input) | |
(insert-and-inherit string)) | |
(defun nrepl-get-next-history-index (direction) | |
(let* ((history nrepl-input-history) | |
(len (length history)) | |
(next (+ nrepl-input-history-index (if (eq direction 'forward) -1 1)))) | |
(cond ((< next 0) -1) | |
((<= len next) len) | |
(t next)))) | |
(defun nrepl-history-replace (direction) | |
"Replace the current input with the next line in DIRECTION. | |
DIRECTION is 'forward' or 'backward' (in the history list)." | |
(let* ((min-pos -1) | |
(max-pos (length nrepl-input-history)) | |
(pos (nrepl-get-next-history-index direction)) | |
(msg)) | |
(cond ((and (< min-pos pos) (< pos max-pos)) | |
(nrepl-replace-input (nth pos nrepl-input-history)) | |
(setq msg (format "History item: %d" pos))) | |
((= pos min-pos) | |
(nrepl-replace-input "") | |
(setq msg "Beginning of history")) | |
((setq msg "End of history" | |
pos (1- pos)))) | |
(message "%s" msg) | |
(setq nrepl-input-history-index pos))) | |
(defun nrepl-previous-input () | |
(interactive) | |
(nrepl-history-replace 'backward)) | |
(defun nrepl-next-input () | |
(interactive) | |
(nrepl-history-replace 'forward)) | |
;;; persistent history | |
(defcustom nrepl-history-size 500 | |
"The maximum number of items to keep in the REPL history." | |
:type 'integer | |
:safe 'integerp | |
:group 'nrepl-mode) | |
(defcustom nrepl-history-file "~/.nrepl-history.eld" | |
"File to save the persistent REPL history to." | |
:type 'string | |
:safe 'stringp | |
:group 'nrepl-mode) | |
(defun nrepl-history-read-filename () | |
"Ask the user which file to use, defaulting `nrepl-history-file`." | |
(read-file-name "Use nREPL history file: " | |
nrepl-history-file)) | |
(defun nrepl-history-read (filename) | |
"Read history from FILE and return it. | |
Does not yet set the input history." | |
(if (file-readable-p filename) | |
(with-temp-buffer | |
(insert-file-contents filename) | |
(read (current-buffer))) | |
'())) | |
(defun nrepl-history-load (&optional filename) | |
"Load history from FILENAME into current session. | |
FILENAME defaults to the value of `nrepl-history-file` but user | |
defined filenames can be used to read special history files. | |
The value of `nrepl-input-history` is set by this function." | |
(interactive (list (nrepl-history-read-filename))) | |
(let ((f (or filename nrepl-history-file))) | |
;; TODO: probably need to set nrepl-input-history-index as well. | |
;; in a fresh connection the newest item in the list is currently | |
;; not available. After sending one input, everything seems to work. | |
(setq nrepl-input-history (nrepl-history-read f)))) | |
(defun nrepl-history-write (filename) | |
"Write history to FILENAME. | |
Currently coding system for writing the contents is hardwired to | |
utf-8-unix." | |
(let* ((mhist (nrepl-histories-merge nrepl-input-history | |
nrepl-input-history-items-added | |
(nrepl-history-read filename))) | |
;; newest items are at the beginning of the list, thus 0 | |
(hist (subseq mhist 0 (min (length mhist) nrepl-history-size)))) | |
(unless (file-writable-p filename) | |
(error (format "History file not writable: %s" filename))) | |
(let ((print-length nil) (print-level nil)) | |
(with-temp-file filename | |
;; TODO: really set cs for output | |
;; TODO: does cs need to be customizable? | |
(insert ";; -*- coding: utf-8-unix -*-\n") | |
(insert ";; Automatically written history of nREPL session\n") | |
(insert ";; Edit at your own risk\n\n") | |
(prin1 (mapcar #'substring-no-properties hist) (current-buffer)))))) | |
(defun nrepl-history-save (&optional filename) | |
"Save the current nREPL input history to FILENAME. | |
FILENAME defaults to the value of `nrepl-history-file`." | |
(interactive (list (nrepl-history-read-filename))) | |
(let* ((file (or filename nrepl-history-file))) | |
(nrepl-history-write file))) | |
(defun nrepl-history-just-save () | |
"Just save the history to `nrepl-history-file`. | |
This function is meant to be used in hooks to avoid lambda | |
constructs." | |
(nrepl-history-save nrepl-history-file)) | |
;; SLIME has different semantics and will not save any duplicates. | |
;; we keep track of how many items were added to the history in the | |
;; current session in nrepl-add-to-input-history and merge only the | |
;; new items with the current history found in the file, which may | |
;; have been changed in the meantime by another session | |
(defun nrepl-histories-merge (session-hist n-added-items file-hist) | |
(append (subseq session-hist 0 n-added-items) | |
file-hist)) | |
;;; | |
(defun nrepl-same-line-p (pos1 pos2) | |
"Return t if buffer positions POS1 and POS2 are on the same line." | |
(save-excursion (goto-char (min pos1 pos2)) | |
(<= (max pos1 pos2) (line-end-position)))) | |
(defun nrepl-bol () | |
"Go to the beginning of line or the prompt." | |
(interactive) | |
(cond ((and (>= (point) nrepl-input-start-mark) | |
(nrepl-same-line-p (point) nrepl-input-start-mark)) | |
(goto-char nrepl-input-start-mark)) | |
(t (beginning-of-line 1)))) | |
(defun nrepl-at-prompt-start-p () | |
;; This will not work on non-current prompts. | |
(= (point) nrepl-input-start-mark)) | |
;;; mode book-keeping | |
(defvar nrepl-mode-hook nil | |
"Hook executed when entering nrepl-mode.") | |
(defvar nrepl-mode-syntax-table | |
(copy-syntax-table clojure-mode-syntax-table)) | |
(defvar nrepl-interaction-mode-map | |
(let ((map (make-sparse-keymap))) | |
(define-key map (kbd "M-.") 'nrepl-jump) | |
(define-key map (kbd "M-,") 'nrepl-jump-back) | |
(define-key map (kbd "M-TAB") 'complete-symbol) | |
(define-key map (kbd "C-M-x") 'nrepl-eval-expression-at-point) | |
(define-key map (kbd "C-x C-e") 'nrepl-eval-last-expression) | |
(define-key map (kbd "C-c C-e") 'nrepl-eval-last-expression) | |
(define-key map (kbd "C-c C-r") 'nrepl-eval-region) | |
(define-key map (kbd "C-c C-n") 'nrepl-eval-ns-form) | |
(define-key map (kbd "C-c C-m") 'nrepl-macroexpand-1-last-expression) | |
(define-key map (kbd "C-c M-m") 'nrepl-macroexpand-all-last-expression) | |
(define-key map (kbd "C-c M-n") 'nrepl-set-ns) | |
(define-key map (kbd "C-c C-d") 'nrepl-doc) | |
(define-key map (kbd "C-c C-z") 'nrepl-switch-to-repl-buffer) | |
(define-key map (kbd "C-c C-k") 'nrepl-load-current-buffer) | |
(define-key map (kbd "C-c C-l") 'nrepl-load-file) | |
(define-key map (kbd "C-c C-b") 'nrepl-interrupt) | |
map)) | |
(defvar nrepl-mode-map | |
(let ((map (make-sparse-keymap))) | |
(set-keymap-parent map clojure-mode-map) | |
(define-key map (kbd "M-.") 'nrepl-jump) | |
(define-key map (kbd "M-,") 'nrepl-jump-back) | |
(define-key map (kbd "RET") 'nrepl-return) | |
(define-key map (kbd "TAB") 'complete-symbol) | |
(define-key map (kbd "C-<return>") 'nrepl-closing-return) | |
(define-key map (kbd "C-j") 'nrepl-newline-and-indent) | |
(define-key map (kbd "C-c C-d") 'nrepl-doc) | |
(define-key map (kbd "C-c C-o") 'nrepl-clear-output) | |
(define-key map (kbd "C-c M-o") 'nrepl-clear-buffer) | |
(define-key map "\C-a" 'nrepl-bol) | |
(define-key map [home] 'nrepl-bol) | |
(define-key map (kbd "C-<up>") 'nrepl-previous-input) | |
(define-key map (kbd "C-<down>") 'nrepl-next-input) | |
(define-key map (kbd "M-p") 'nrepl-previous-input) | |
(define-key map (kbd "M-n") 'nrepl-next-input) | |
(define-key map (kbd "C-c C-b") 'nrepl-interrupt) | |
map)) | |
(defun clojure-enable-nrepl () | |
(nrepl-interaction-mode t)) | |
;;;###autoload | |
(define-minor-mode nrepl-interaction-mode | |
"Minor mode for nrepl interaction from a Clojure buffer." | |
nil | |
" nREPL" | |
nrepl-interaction-mode-map | |
(make-local-variable 'completion-at-point-functions) | |
(add-to-list 'completion-at-point-functions | |
'nrepl-complete-at-point)) | |
(defun nrepl-mode () | |
"Major mode for nREPL interactions." | |
(interactive) | |
(kill-all-local-variables) | |
(use-local-map nrepl-mode-map) | |
(setq mode-name "nREPL" | |
major-mode 'nrepl-mode) | |
(make-local-variable 'completion-at-point-functions) | |
(add-to-list 'completion-at-point-functions | |
'nrepl-complete-at-point) | |
(set-syntax-table nrepl-mode-syntax-table) | |
(nrepl-eldoc-enable-in-current-buffer) | |
(when nrepl-history-file | |
(nrepl-history-load nrepl-history-file) | |
(make-local-variable 'kill-buffer-hook) | |
(add-hook 'kill-buffer-hook 'nrepl-history-just-save) | |
(add-hook 'kill-emacs-hook 'nrepl-history-just-save)) | |
(run-mode-hooks 'nrepl-mode-hook)) | |
;;; communication | |
(defcustom nrepl-lein-command | |
"lein" | |
"The command used to execute leiningen 2.x." | |
:type 'string | |
:group 'nrepl-mode) | |
(defcustom nrepl-server-command | |
(if (or (locate-file nrepl-lein-command exec-path) | |
(locate-file (format "%s.bat" nrepl-lein-command) exec-path)) | |
(format "%s repl :headless" nrepl-lein-command) | |
(format "echo \"%s repl :headless\" | $SHELL -l" nrepl-lein-command)) | |
"The command used to start the nREPL via nrepl-jack-in. | |
For a remote nREPL server lein must be in your PATH. The remote | |
proc is launched via sh rather than bash, so it might be necessary | |
to specific the full path to it. Localhost is assumed." | |
:type 'string | |
:group 'nrepl-mode) | |
(defun nrepl-show-maximum-output () | |
"Put the end of the buffer at the bottom of the window." | |
(when (eobp) | |
(let ((win (get-buffer-window (current-buffer)))) | |
(when win | |
(with-selected-window win | |
(set-window-point win (point-max)) | |
(recenter -1)))))) | |
(defmacro nrepl-save-marker (marker &rest body) | |
(let ((pos (make-symbol "pos"))) | |
`(let ((,pos (marker-position ,marker))) | |
(prog1 (progn . ,body) | |
(set-marker ,marker ,pos))))) | |
(put 'nrepl-save-marker 'lisp-indent-function 1) | |
(defun nrepl-insert-prompt (namespace) | |
"Insert the prompt (before markers!). | |
Set point after the prompt. | |
Return the position of the prompt beginning." | |
(goto-char nrepl-input-start-mark) | |
(nrepl-save-marker nrepl-output-start | |
(nrepl-save-marker nrepl-output-end | |
(unless (bolp) (insert-before-markers "\n")) | |
(let ((prompt-start (point)) | |
(prompt (format "%s> " namespace))) | |
(nrepl-propertize-region | |
'(face nrepl-prompt-face read-only t intangible t | |
nrepl-prompt t | |
rear-nonsticky (nrepl-prompt read-only face intangible)) | |
(insert-before-markers prompt)) | |
(set-marker nrepl-prompt-start-mark prompt-start) | |
prompt-start)))) | |
(defun nrepl-emit-output-at-pos (buffer string position &optional bol) | |
;; insert STRING and mark it as output | |
(with-current-buffer buffer | |
(save-excursion | |
(nrepl-save-marker nrepl-output-start | |
(nrepl-save-marker nrepl-output-end | |
(goto-char position) | |
(when (and bol (not (bolp))) (insert-before-markers "\n")) | |
(nrepl-propertize-region `(face nrepl-output-face | |
rear-nonsticky (face)) | |
(insert-before-markers string) | |
(when (and (= (point) nrepl-prompt-start-mark) | |
(not (bolp))) | |
(insert-before-markers "\n") | |
(set-marker nrepl-output-end (1- (point)))))))) | |
(nrepl-show-maximum-output))) | |
(defun nrepl-emit-interactive-output (string) | |
(with-current-buffer "*nrepl*" | |
(nrepl-emit-output-at-pos (current-buffer) string (1- (nrepl-input-line-beginning-position)) t))) | |
(defun nrepl-emit-output (buffer string &optional bol) | |
(with-current-buffer buffer | |
(nrepl-emit-output-at-pos buffer string nrepl-input-start-mark bol))) | |
(defun nrepl-emit-prompt (buffer) | |
(with-current-buffer buffer | |
(save-excursion | |
(nrepl-save-marker nrepl-output-start | |
(nrepl-save-marker nrepl-output-end | |
(nrepl-insert-prompt nrepl-buffer-ns)))) | |
(nrepl-show-maximum-output))) | |
(defun nrepl-emit-result (buffer string &optional bol) | |
;; insert STRING and mark it as evaluation result | |
(with-current-buffer buffer | |
(save-excursion | |
(nrepl-save-marker nrepl-output-start | |
(nrepl-save-marker nrepl-output-end | |
(goto-char nrepl-input-start-mark) | |
(when (and bol (not (bolp))) (insert-before-markers "\n")) | |
(nrepl-propertize-region `(face nrepl-result-face | |
rear-nonsticky (face)) | |
(insert-before-markers string))))) | |
(nrepl-show-maximum-output))) | |
(defun nrepl-dispatch (response) | |
"Dispatch the response to associated callback." | |
(nrepl-dbind-response response (id) | |
(let ((callback (gethash id nrepl-requests))) | |
(if callback | |
(funcall callback response))))) | |
(defun nrepl-net-decode () | |
"Decode the data in the current buffer and remove the processed data from the | |
buffer if the decode successful." | |
(let* ((start (point-min)) | |
(end (point-max)) | |
(data (buffer-substring start end))) | |
(prog1 | |
(nrepl-decode data) | |
(delete-region start end)))) | |
(defun nrepl-net-process-input (process) | |
"Process all complete messages. | |
Assume that any error during decoding indicates an incomplete message." | |
(with-current-buffer (process-buffer process) | |
(ignore-errors | |
(while (> (buffer-size) 1) | |
(let ((responses (nrepl-net-decode))) | |
(dolist (response responses) | |
(nrepl-dispatch response))))))) | |
(defun nrepl-net-filter (process string) | |
"Decode the message(s) and dispatch." | |
(with-current-buffer (process-buffer process) | |
(goto-char (point-max)) | |
(insert string)) | |
(nrepl-net-process-input process)) | |
(defun nrepl-sentinel (process message) | |
(message "nrepl connection closed: %s" message) | |
(kill-buffer (process-buffer process))) | |
(defun nrepl-write-message (process message) | |
(process-send-string process message)) | |
;;; repl interaction | |
(defun nrepl-in-input-area-p () | |
(<= nrepl-input-start-mark (point))) | |
(defun nrepl-current-input (&optional until-point-p) | |
"Return the current input as string. | |
The input is the region from after the last prompt to the end of | |
buffer." | |
(buffer-substring-no-properties nrepl-input-start-mark | |
(if until-point-p | |
(point) | |
(point-max)))) | |
(defun nrepl-previous-prompt () | |
"Move backward to the previous prompt." | |
(interactive) | |
(nrepl-find-prompt t)) | |
(defun nrepl-next-prompt () | |
"Move forward to the next prompt." | |
(interactive) | |
(nrepl-find-prompt)) | |
(defun nrepl-find-prompt (&optional backward) | |
(let ((origin (point)) | |
(prop 'nrepl-prompt)) | |
(while (progn | |
(nrepl-search-property-change prop backward) | |
(not (or (nrepl-end-of-proprange-p prop) (bobp) (eobp))))) | |
(unless (nrepl-end-of-proprange-p prop) | |
(goto-char origin)))) | |
(defun nrepl-search-property-change (prop &optional backward) | |
(cond (backward | |
(goto-char (previous-single-char-property-change (point) prop))) | |
(t | |
(goto-char (next-single-char-property-change (point) prop))))) | |
(defun nrepl-end-of-proprange-p (property) | |
(and (get-char-property (max 1 (1- (point))) property) | |
(not (get-char-property (point) property)))) | |
(defun nrepl-mark-input-start () | |
(set-marker nrepl-input-start-mark (point) (current-buffer))) | |
(defun nrepl-mark-output-start () | |
(set-marker nrepl-output-start (point)) | |
(set-marker nrepl-output-end (point))) | |
(defun nrepl-mark-output-end () | |
(add-text-properties nrepl-output-start nrepl-output-end | |
'(face nrepl-output-face | |
rear-nonsticky (face)))) | |
;;; server messages | |
(defun nrepl-current-session () | |
(with-current-buffer "*nrepl-connection*" | |
nrepl-session)) | |
(defun nrepl-send-request (request callback) | |
(let* ((request-id (number-to-string (incf nrepl-request-counter))) | |
(message (nrepl-bencode (append (list "id" request-id) request)))) | |
(puthash request-id callback nrepl-requests) | |
(nrepl-write-message "*nrepl-connection*" message))) | |
(defun nrepl-create-client-session (callback) | |
(nrepl-send-request '("op" "clone") | |
callback)) | |
(defun nrepl-send-stdin (input callback) | |
(nrepl-send-request (list "op" "stdin" | |
"stdin" input | |
"session" (nrepl-current-session)) | |
callback)) | |
(defun nrepl-send-interrupt (pending-request-id callback) | |
(nrepl-send-request (list "op" "interrupt" | |
"session" (nrepl-current-session) | |
"interrupt-id" pending-request-id) | |
callback)) | |
(defun nrepl-eval-request (input &optional ns) | |
(append (if ns (list "ns" ns)) | |
(list | |
"op" "eval" | |
"session" (nrepl-current-session) | |
"code" input))) | |
(defun nrepl-send-string (input callback &optional ns) | |
(nrepl-send-request (nrepl-eval-request input ns) callback)) | |
(defun nrepl-sync-request-handler (buffer) | |
(nrepl-make-response-handler buffer | |
(lambda (buffer value) | |
(setq nrepl-sync-response | |
(plist-put nrepl-sync-response :value value))) | |
(lambda (buffer out) | |
(let ((so-far (plist-get nrepl-sync-response :stdout))) | |
(setq nrepl-sync-response | |
(plist-put nrepl-sync-response | |
:stdout (concat so-far out))))) | |
(lambda (buffer err) | |
(let ((so-far (plist-get nrepl-sync-response :stderr))) | |
(setq nrepl-sync-response | |
(plist-put nrepl-sync-response | |
:stderr (concat so-far err))))) | |
(lambda (buffer) | |
(setq nrepl-sync-response | |
(plist-put nrepl-sync-response :done t))))) | |
(defun nrepl-send-request-sync (request) | |
"Send a request to the backend synchronously (discouraged). | |
The result is a plist with keys :value, :stderr and :stdout." | |
(with-current-buffer "*nrepl-connection*" | |
(setq nrepl-sync-response nil) | |
(nrepl-send-request request (nrepl-sync-request-handler (current-buffer))) | |
(while (null nrepl-sync-response) | |
(accept-process-output nil 0 5)) | |
nrepl-sync-response)) | |
(defun nrepl-send-string-sync (input &optional ns) | |
(nrepl-send-request-sync (nrepl-eval-request input ns))) | |
(defun nrepl-send-input (&optional newline) | |
"Goto to the end of the input and send the current input. | |
If NEWLINE is true then add a newline at the end of the input." | |
(unless (nrepl-in-input-area-p) | |
(error "No input at point.")) | |
(goto-char (point-max)) | |
(let ((end (point))) ; end of input, without the newline | |
(nrepl-add-to-input-history (buffer-substring nrepl-input-start-mark end)) | |
(when newline | |
(insert "\n") | |
(nrepl-show-maximum-output)) | |
(let ((inhibit-modification-hooks t)) | |
(add-text-properties nrepl-input-start-mark | |
(point) | |
`(nrepl-old-input | |
,(incf nrepl-old-input-counter)))) | |
(let ((overlay (make-overlay nrepl-input-start-mark end))) | |
;; These properties are on an overlay so that they won't be taken | |
;; by kill/yank. | |
(overlay-put overlay 'read-only t) | |
(overlay-put overlay 'face 'nrepl-input-face))) | |
(let ((input (nrepl-current-input))) | |
(goto-char (point-max)) | |
(nrepl-mark-input-start) | |
(nrepl-mark-output-start) | |
(setq nrepl-input-history-index -1) | |
(nrepl-send-string input (nrepl-handler (current-buffer)) nrepl-buffer-ns))) | |
(defun nrepl-newline-and-indent () | |
"Insert a newline, then indent the next line. | |
Restrict the buffer from the prompt for indentation, to avoid being | |
confused by strange characters (like unmatched quotes) appearing | |
earlier in the buffer." | |
(interactive) | |
(save-restriction | |
(narrow-to-region nrepl-prompt-start-mark (point-max)) | |
(insert "\n") | |
(lisp-indent-line))) | |
(defun nrepl-input-complete-p (start end) | |
"Return t if the region from START to END contains a complete sexp." | |
(save-excursion | |
(goto-char start) | |
(cond ((looking-at "\\s *['`#]?[(\"]") | |
(ignore-errors | |
(save-restriction | |
(narrow-to-region start end) | |
;; Keep stepping over blanks and sexps until the end of | |
;; buffer is reached or an error occurs. Tolerate extra | |
;; close parens. | |
(loop do (skip-chars-forward " \t\r\n)") | |
until (eobp) | |
do (forward-sexp)) | |
t))) | |
(t t)))) | |
(defun nrepl-return (&optional end-of-input) | |
"Evaluate the current input string, or insert a newline. | |
Send the current input ony if a whole expression has been entered, | |
i.e. the parenthesis are matched. | |
With prefix argument send the input even if the parenthesis are not | |
balanced." | |
(interactive "P") | |
(cond | |
((nrepl-input-complete-p nrepl-input-start-mark (point-max)) | |
(nrepl-send-input t)) | |
(t | |
(nrepl-newline-and-indent)))) | |
(defun nrepl-closing-return () | |
"Evaluate the current input string after closing all open lists." | |
(interactive) | |
(goto-char (point-max)) | |
(save-restriction | |
(narrow-to-region nrepl-input-start-mark (point)) | |
(while (ignore-errors (save-excursion (backward-up-list 1)) t) | |
(insert ")"))) | |
(nrepl-return)) | |
(defvar nrepl-clear-buffer-hook) | |
(defun nrepl-clear-buffer () | |
"Delete the output generated by the Clojure process." | |
(interactive) | |
(let ((inhibit-read-only t)) | |
(delete-region (point-min) nrepl-prompt-start-mark) | |
(delete-region nrepl-output-start nrepl-output-end) | |
(when (< (point) nrepl-input-start-mark) | |
(goto-char nrepl-input-start-mark)) | |
(recenter t)) | |
(run-hooks 'nrepl-clear-buffer-hook)) | |
(defun nrepl-input-line-beginning-position () | |
(save-excursion | |
(goto-char nrepl-input-start-mark) | |
(line-beginning-position))) | |
(defun nrepl-clear-output () | |
"Delete the output inserted since the last input." | |
(interactive) | |
(let ((start (save-excursion | |
(nrepl-previous-prompt) | |
(ignore-errors (forward-sexp)) | |
(forward-line) | |
(point))) | |
(end (1- (nrepl-input-line-beginning-position)))) | |
(when (< start end) | |
(let ((inhibit-read-only t)) | |
(delete-region start end) | |
(save-excursion | |
(goto-char start) | |
(insert ";;; output cleared")))))) | |
(defun nrepl-find-ns () | |
(or (save-restriction | |
(widen) | |
(clojure-find-ns)) | |
"user")) | |
(defun nrepl-current-ns () | |
"Return the ns in the current context. | |
If `nrepl-buffer-ns' has a value then return that, otherwise | |
search for and read a `ns' form." | |
(let ((ns nrepl-buffer-ns)) | |
(or (and (string= ns "user") | |
(nrepl-find-ns)) | |
ns))) | |
;; Words of inspiration | |
(defun nrepl-user-first-name () | |
(let ((name (if (string= (user-full-name) "") | |
(user-login-name) | |
(user-full-name)))) | |
(string-match "^[^ ]*" name) | |
(capitalize (match-string 0 name)))) | |
(defvar nrepl-words-of-inspiration | |
`("The best way to predict the future is to invent it. -Alan Kay" | |
"A point of view is worth 80 IQ points. -Alan Kay" | |
"Simple things should be simple, complex things should be possible. -Alan Kay" | |
"Programming is not about typing... it's about thinking. -Rich Hickey" | |
"Take this nREPL, brother, and may it serve you well." | |
,(format "%s, this could be the start of a beautiful program." | |
(nrepl-user-first-name)))) | |
(defun nrepl-random-words-of-inspiration () | |
(eval (nth (random (length nrepl-words-of-inspiration)) | |
nrepl-words-of-inspiration))) | |
(defun nrepl-insert-banner (ns) | |
(when (zerop (buffer-size)) | |
(let ((welcome (concat "; nREPL " nrepl-version))) | |
(insert welcome))) | |
(goto-char (point-max)) | |
(nrepl-mark-output-start) | |
(nrepl-mark-input-start) | |
(nrepl-insert-prompt ns)) | |
(defun nrepl-init-repl-buffer (connection buffer &optional noprompt) | |
(with-current-buffer buffer | |
(unless (eq major-mode 'nrepl-mode) | |
(nrepl-mode)) | |
(nrepl-reset-markers) | |
(unless noprompt | |
(nrepl-insert-banner nrepl-buffer-ns)) | |
(current-buffer))) | |
(defun nrepl-repl-buffer (&optional noprompt) | |
"Return the repl buffer, create if necessary." | |
(let ((buffer (get-buffer "*nrepl*"))) | |
(or (if (buffer-live-p buffer) buffer) | |
(let ((connection (get-process "*nrepl-connection*"))) | |
(nrepl-init-repl-buffer connection (get-buffer-create "*nrepl*")))))) | |
(defun nrepl-switch-to-repl-buffer () | |
"Select the repl buffer, when possible in an existing window. | |
Hint: You can use `display-buffer-reuse-frames' and | |
`special-display-buffer-names' to customize the frame in which | |
the buffer should appear." | |
(interactive) | |
(if (not (get-buffer "*nrepl-connection*")) | |
(message "No active nREPL connection.") | |
(pop-to-buffer (nrepl-repl-buffer)) | |
(goto-char (point-max)))) | |
(defun nrepl-set-ns (ns) | |
"Switch the namespace of the nREPL buffer to ns." | |
(interactive (list (nrepl-current-ns))) | |
(with-current-buffer "*nrepl*" | |
(nrepl-send-string (format "(in-ns '%s)" ns) (nrepl-handler (current-buffer))))) | |
(defun nrepl-symbol-at-point () | |
"Return the name of the symbol at point, otherwise nil." | |
(let ((str (thing-at-point 'symbol))) | |
(and str | |
(not (equal str "")) | |
(substring-no-properties str)))) | |
;; this is horrible, but with async callbacks we can't rely on dynamic scope | |
(defvar nrepl-ido-ns nil) | |
(defun nrepl-ido-form (ns) | |
`(concat (if (find-ns (symbol ,ns)) | |
(map name (keys (ns-interns (symbol ,ns))))) | |
(if (not= "" ,ns) [".."]) | |
(->> (all-ns) | |
(map (fn [n] | |
(re-find (re-pattern (str "^" (if (not= ,ns "") | |
(str ,ns "\\.")) | |
"[^\\.]+")) | |
(str n)))) | |
(filter identity) | |
(map (fn [n] (str n "/"))) | |
(into (hash-set))))) | |
(defun nrepl-ido-up-ns (ns) | |
(mapconcat 'identity (butlast (split-string ns "\\.")) ".")) | |
(defun nrepl-ido-select (selected targets callback) | |
;; TODO: immediate RET gives "" as selected for some reason | |
;; this is an OK workaround though | |
(cond ((equal "" selected) | |
(nrepl-ido-select (car targets) targets callback)) | |
((equal "/" (substring selected -1)) ; selected a namespace | |
(nrepl-ido-read-var (substring selected 0 -1) callback)) | |
((equal ".." selected) | |
(nrepl-ido-read-var (nrepl-ido-up-ns nrepl-ido-ns) callback)) | |
(t (funcall callback (concat nrepl-ido-ns "/" selected))))) | |
(defun nrepl-ido-read-var-handler (ido-callback buffer) | |
(lexical-let ((ido-callback ido-callback)) | |
(nrepl-make-response-handler buffer | |
(lambda (buffer value) | |
(let* ((targets (car (read-from-string value))) | |
(selected (ido-completing-read "Var: " targets nil t))) | |
(nrepl-ido-select selected targets ido-callback))) | |
nil nil nil))) | |
(defun nrepl-ido-read-var (ns ido-callback) | |
;; Have to be stateful =( | |
(setq nrepl-ido-ns ns) | |
(nrepl-send-string (prin1-to-string (nrepl-ido-form nrepl-ido-ns)) | |
(nrepl-ido-read-var-handler ido-callback (current-buffer)) | |
nrepl-buffer-ns)) | |
(defun nrepl-operator-before-point () | |
(ignore-errors | |
(save-excursion | |
(backward-up-list 1) | |
(down-list 1) | |
(nrepl-symbol-at-point)))) | |
(defun nrepl-read-symbol-name (prompt callback &optional query) | |
"Either read a symbol name or choose the one at point. | |
The user is prompted if a prefix argument is in effect, if there is no | |
symbol at point, or if QUERY is non-nil." | |
(let ((symbol-name (nrepl-symbol-at-point))) | |
(cond ((not (or current-prefix-arg query (not symbol-name))) | |
(funcall callback symbol-name)) | |
(ido-mode (nrepl-ido-read-var nrepl-buffer-ns callback)) | |
(t (funcall callback (read-from-minibuffer prompt symbol-name)))))) | |
(defun nrepl-doc-handler (symbol) | |
(let ((form (format "(clojure.repl/doc %s)" symbol)) | |
(doc-buffer (nrepl-popup-buffer "*nREPL doc*" t))) | |
(nrepl-send-string form | |
(nrepl-popup-eval-out-handler doc-buffer) | |
nrepl-buffer-ns))) | |
(defun nrepl-doc (query) | |
"Open a window with the docstring for the given entry. | |
Defaults to the symbol at point. With prefix arg or no symbol | |
under point, prompts for a var." | |
(interactive "P") | |
(nrepl-read-symbol-name "Symbol: " 'nrepl-doc-handler query)) | |
;; TODO: implement reloading ns | |
(defun nrepl-eval-load-file (form) | |
(let ((buffer (current-buffer))) | |
(nrepl-send-string form (nrepl-interactive-eval-handler buffer)))) | |
(defun nrepl-load-file (filename) | |
"Load the clojure file FILENAME." | |
(interactive (list | |
(read-file-name "Load file: " nil nil | |
nil (if (buffer-file-name) | |
(file-name-nondirectory | |
(buffer-file-name)))))) | |
(let ((fn (replace-regexp-in-string | |
"\\\\" "\\\\\\\\" | |
(convert-standard-filename (expand-file-name filename))))) | |
(nrepl-eval-load-file | |
(format "(clojure.core/load-file \"%s\")\n(in-ns '%s)\n" | |
fn (nrepl-find-ns))) | |
(message "Loading %s..." fn))) | |
(defun nrepl-load-current-buffer () | |
"Load current buffer's file." | |
(interactive) | |
(check-parens) | |
(unless buffer-file-name | |
(error "Buffer %s is not associated with a file." (buffer-name))) | |
(when (and (buffer-modified-p) | |
(y-or-n-p (format "Save file %s? " (buffer-file-name)))) | |
(save-buffer)) | |
(nrepl-load-file (buffer-file-name))) | |
;;; interrupt | |
(defun nrepl-interrupt-handler (buffer) | |
(nrepl-make-response-handler buffer nil nil nil nil)) | |
(defun nrepl-hash-keys (hashtable) | |
(let ((keys '())) | |
(maphash (lambda (k v) (setq keys (cons k keys))) hashtable) | |
keys)) | |
(defun nrepl-interrupt () | |
"Interrupt any pending evaluations." | |
(interactive) | |
(let ((pending-request-ids (nrepl-hash-keys nrepl-requests))) | |
(dolist (request-id pending-request-ids) | |
(nrepl-send-interrupt request-id (nrepl-interrupt-handler (current-buffer)))))) | |
;;; server | |
(defun nrepl-server-filter (process output) | |
(with-current-buffer (process-buffer process) | |
(insert output)) | |
(when (string-match "nREPL server started on port \\([0-9]+\\)" output) | |
(let ((port (string-to-number (match-string 1 output)))) | |
(message (format "nREPL server started on %s" port)) | |
(nrepl "localhost" port)))) | |
(defun nrepl-server-sentinel (process event) | |
(let* ((b (process-buffer process)) | |
(problem (if (and b (buffer-live-p b)) | |
(with-current-buffer b | |
(buffer-substring (point-min) (point-max)))))) | |
(when b | |
(kill-buffer b)) | |
(if (string-match "Wrong number of arguments to repl task." problem) | |
(error "nrepl.el requires Leiningen 2.x") | |
(error "Could not start nREPL server: %s" problem)))) | |
;;;###autoload | |
(defun nrepl-enable-on-existing-clojure-buffers () | |
(interactive) | |
(add-hook 'clojure-mode-hook 'clojure-enable-nrepl) | |
(add-hook 'clojurescript-mode-hook 'clojure-enable-nrepl) | |
(save-window-excursion | |
(dolist (buffer (buffer-list)) | |
(with-current-buffer buffer | |
(when (or (eq major-mode 'clojure-mode) | |
(eq major-mode 'clojurescript-mode)) | |
(clojure-enable-nrepl)))))) | |
;;;###autoload | |
(defun nrepl-jack-in (prompt-project) | |
(interactive "P") | |
(let* ((cmd (if prompt-project | |
(format "cd %s && %s" (ido-read-directory-name "Project: ") | |
nrepl-server-command) | |
nrepl-server-command)) | |
(process (start-process-shell-command | |
"nrepl-server" "*nrepl-server*" cmd))) | |
(set-process-filter process 'nrepl-server-filter) | |
(set-process-sentinel process 'nrepl-server-sentinel) | |
(set-process-coding-system process 'utf-8-unix 'utf-8-unix) | |
(message "Starting nREPL server..."))) | |
;;; client | |
(defun nrepl-new-session-handler (buffer) | |
(lexical-let ((buffer buffer)) | |
(lambda (response) | |
(nrepl-dbind-response response (id new-session) | |
(cond (new-session | |
(with-current-buffer buffer | |
(message "Connected. %s" (nrepl-random-words-of-inspiration)) | |
(setq nrepl-session new-session) | |
(remhash id nrepl-requests) | |
(run-hooks 'nrepl-connected-hook)))))))) | |
(defun nrepl-connect (host port) | |
(message "Connecting to nREPL on %s:%s..." host port) | |
(let ((process (open-network-stream "nrepl" "*nrepl-connection*" host | |
port))) | |
(set-process-filter process 'nrepl-net-filter) | |
(set-process-sentinel process 'nrepl-sentinel) | |
(set-process-coding-system process 'utf-8-unix 'utf-8-unix) | |
(nrepl-create-client-session (nrepl-new-session-handler (process-buffer process))) | |
process)) | |
;;;###autoload | |
(add-hook 'nrepl-connected-hook 'nrepl-enable-on-existing-clojure-buffers) | |
;;;###autoload | |
(defun nrepl (host port) | |
(interactive "MHost: \nnPort: ") | |
(let ((nrepl-buffer (switch-to-buffer-other-window (generate-new-buffer-name "*nrepl*"))) | |
(process (nrepl-connect host port))) | |
(nrepl-init-repl-buffer process nrepl-buffer))) | |
(provide 'nrepl) | |
;;; nrepl.el ends here |