You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
|
|
;;; le-clojure.el --- lispy support for Clojure. -*- lexical-binding: t -*-
;; Copyright (C) 2014-2018 Oleh Krehel
;; This file is not part of GNU Emacs
;; This file 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, 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.
;; For a full copy of the GNU General Public License;; see <http://www.gnu.org/licenses/>.
;;; Commentary:;;
;;; Code:
;;* Requires(require 'lispy)(require 'cider-client nil t)(require 'cider-connection nil t)(require 'cider-eval nil t)(require 'cider-find nil t)
(defcustom lispy-clojure-eval-method 'cider "REPL used for eval." :type '(choice (const :tag "CIDER" cider) (const :tag "UNREPL" spiral)) :group 'lispy)
(defcustom lispy-cider-connect-method 'cider-jack-in "Function used to create a CIDER connection." :type '(choice (const cider-jack-in) (const cider-connect) (function :tag "Custom")) :group 'lispy)
;;* Namespace(defvar lispy--clojure-ns "user" "Store the last evaluated *ns*.")
(defun lispy--clojure-detect-ns () "When there's only one (ns ...) in the buffer, use it." (save-excursion (goto-char (point-min)) (when (re-search-forward clojure-namespace-name-regex nil t) (let ((ns (match-string-no-properties 4))) (when (not (re-search-forward clojure-namespace-name-regex nil t)) (setq lispy--clojure-ns ns))))))
;;* User wrapper for eval(defvar lispy--clojure-middleware-loaded-p nil "Nil if the Clojure middleware in \"lispy-clojure.clj\" wasn't loaded yet.")
(defun lispy-eval-clojure (e-str) "User facing eval." (lispy--clojure-detect-ns) (let (c-str) (unless (stringp e-str) (setq e-str (lispy--string-dwim)) (setq c-str (let ((deactivate-mark nil) (lispy-ignore-whitespace t)) (save-mark-and-excursion (lispy--out-backward 1) (deactivate-mark) (lispy--string-dwim))))) (let ((f-str (cond ((eq major-mode 'clojurescript-mode) e-str) (lispy--clojure-middleware-loaded-p (format (if (eq this-command 'special-lispy-eval) "(lispy-clojure/pp (lispy-clojure/reval %S %S :file %S :line %S))" "(lispy-clojure/reval %S %S :file %S :line %S)") e-str c-str (buffer-file-name) (line-number-at-pos))) (t e-str)))) (cond ((eq current-prefix-arg 7) (kill-new f-str)) ((and (eq current-prefix-arg 0) (lispy--eval-clojure "(lispy-clojure/shadow-unmap *ns*)") nil)) ((eq lispy-clojure-eval-method 'spiral) (lispy--eval-clojure-spiral e-str)) (t (lispy--eval-clojure f-str e-str))))))
;;* Start REPL wrapper for eval(defvar lispy--clojure-hook-lambda nil "Store a lambda to call.")
(defun lispy--clojure-eval-hook-lambda () "Call `lispy--clojure-hook-lambda'." (when lispy--clojure-hook-lambda (funcall lispy--clojure-hook-lambda) (setq lispy--clojure-hook-lambda nil)) (remove-hook 'nrepl-connected-hook 'lispy--clojure-eval-hook-lambda))
(eval-after-load 'cider '(progn (cider-add-to-alist 'cider-jack-in-dependencies "org.tcrawley/dynapath" "0.2.5") (cider-add-to-alist 'cider-jack-in-dependencies "com.cemerick/pomegranate" "0.4.0") (cider-add-to-alist 'cider-jack-in-dependencies "compliment" "0.3.6") (cider-add-to-alist 'cider-jack-in-dependencies "me.raynes/fs" "1.4.6")))
(declare-function cider-connections "ext:cider-connection")
(defun lispy--eval-clojure (str &optional add-output) "Eval STR as Clojure code.
The result is a string.
When ADD-OUTPUT is non-nil, add the standard output to the result."
(require 'cider) (unless (eq major-mode 'clojurescript-mode) (add-hook 'cider-connected-hook #'lispy--clojure-middleware-load)) (let (deactivate-mark) (if (null (car (cider-connections))) (progn (setq lispy--clojure-hook-lambda `(lambda () (set-window-configuration ,(current-window-configuration)) (message (lispy--eval-clojure-1 ,str ,add-output)))) (add-hook 'nrepl-connected-hook 'lispy--clojure-eval-hook-lambda t) (call-interactively lispy-cider-connect-method) (format "Starting CIDER using %s ..." lispy-cider-connect-method)) (unless lispy--clojure-middleware-loaded-p (lispy--clojure-middleware-load)) (lispy--eval-clojure-1 str add-output))))
;;* Base eval(defvar lispy--clojure-errorp nil)(defun lispy--eval-clojure-1 (str add-output) (setq lispy--clojure-errorp nil) (or (and (stringp add-output) (lispy--eval-clojure-handle-ns add-output)) (let* (pp (stra (if (setq pp (string-match "\\`(lispy-clojure/\\(pp\\|reval\\)" str)) str (format "(do %s)" str))) (res (lispy--eval-nrepl-clojure stra lispy--clojure-ns)) (status (nrepl-dict-get res "status")) (res (cond ((or (member "namespace-not-found" status)) (lispy--eval-nrepl-clojure stra)) ((member "eval-error" status) (setq lispy--clojure-errorp t) res) (t res))) (val (nrepl-dict-get res "value")) out) (cond ((null val) (lispy--clojure-pretty-string (nrepl-dict-get res "err")))
(add-output (concat (if (setq out (nrepl-dict-get res "out")) (concat (propertize out 'face 'font-lock-string-face) "\n") "") (lispy--clojure-pretty-string (if pp (condition-case nil (string-trim (read val)) (error val)) val))))
(t (lispy--clojure-pretty-string val))))))
(defun lispy--eval-clojure-handle-ns (str) (when (or (string-match "\\`(ns \\([a-z-_0-9\\.]+\\)" str) (string-match "\\`(in-ns '\\([a-z-_0-9\\.]+\\)" str)) (setq lispy--clojure-ns (match-string 1 str)) (let* ((res (lispy--eval-nrepl-clojure str "user")) (status (nrepl-dict-get res "status"))) (when (member "eval-error" status) (error (nrepl-dict-get res "err")))) lispy--clojure-ns))
;;* Handle NREPL version incompat(defun lispy--eval-nrepl-clojure (str &optional namespace) (nrepl-sync-request:eval str (cider-current-connection) namespace))
(defvar spiral-conn-id)(defvar spiral-aux-sync-request-timeout)(declare-function spiral-projects-as-list "ext:spiral-project")(declare-function spiral-pending-eval-add "ext:spiral-project")(declare-function spiral-ast-unparse-to-string "ext:spiral-ast")(declare-function spiral-loop--send "ext:spiral-loop")
(defun lispy--eval-clojure-spiral (str) (let* ((start (current-time)) (repl-buf (cdr (assoc :repl-buffer (car (spiral-projects-as-list))))) (conn-id (with-current-buffer repl-buf spiral-conn-id)) (unparse-no-properties (lambda (node) (substring-no-properties (spiral-ast-unparse-to-string node)))) stdout result) (spiral-loop--send conn-id :aux str) (spiral-pending-eval-add :aux conn-id :status :sent :eval-callback (lambda (eval-payload) (setq result (funcall unparse-no-properties eval-payload))) :stdout-callback (lambda (stdout-payload &rest _) (setq stdout (concat stdout (funcall unparse-no-properties stdout-payload))))) (while (and (not result) (not (input-pending-p)) ;; do not hang UI (or (not spiral-aux-sync-request-timeout) (< (cadr (time-subtract (current-time) start)) spiral-aux-sync-request-timeout))) (accept-process-output nil 0.01)) (if stdout (concat stdout "\n" result) result)))
;;* Rest(defvar cider--debug-mode-response)(declare-function cider--debug-mode "ext:cider-debug")(defvar nrepl-ongoing-sync-request)
(defun lispy--clojure-debug-quit () (let ((pt (save-excursion (if (lispy--leftp) (forward-list) (lispy--out-forward 1)) (lispy-up 1) (point))) (str (format "(do %s)" (mapconcat (lambda (x) (format "(def %s %s)" (car x) (cadr x))) (nrepl-dict-get cider--debug-mode-response "locals") "\n")))) (nrepl-send-request (list "op" "debug-input" "input" ":quit" "key" (nrepl-dict-get cider--debug-mode-response "key")) (lambda (_response)) (cider-current-connection)) (lispy--eval-clojure str) (ignore-errors (let ((nrepl-ongoing-sync-request nil)) (cider--debug-mode -1))) (goto-char pt)))
(defun lispy--clojure-resolve (symbol) "Return resolved SYMBOL.
Return 'special or 'keyword appropriately.Otherwise try to resolve in current namespace first.If it doesn't work, try to resolve in all available namespaces."
(let ((str (lispy--eval-clojure (format "(lispy-clojure/resolve-sym '%s)" symbol)))) (cond (lispy--clojure-errorp (user-error str))
((string-match "^#'\\(.*\\)$" str) (match-string 1 str)) (t (read str)))))
(defun lispy--clojure-symbol-to-args (symbol) (cond ((string= symbol ".") (lispy--clojure-dot-args)) ((string-match "\\`\\(.*\\)\\.\\'" symbol) (lispy--clojure-constructor-args (match-string 1 symbol))) (t (let ((sym (lispy--clojure-resolve symbol))) (cond ((eq sym 'special) (read (lispy--eval-clojure (format "(lispy-clojure/arglist '%s)" symbol)))) ((eq sym 'keyword) (list "[map]")) ((eq sym 'undefined) (error "Undefined")) ((and (listp sym) (eq (car sym) 'variable)) (list "variable")) (t (read (lispy--eval-clojure (format "(lispy-clojure/arglist '%s)" symbol)))))))))
(defun lispy--clojure-args (symbol) "Return a pretty string with arguments for SYMBOL.
Besides functions, handles specials, keywords, maps, vectors and sets."
(let ((args (lispy--clojure-symbol-to-args symbol))) (if (listp args) (format "(%s %s)" (propertize symbol 'face 'lispy-face-hint) (mapconcat #'identity (mapcar (lambda (x) (propertize (downcase x) 'face 'lispy-face-req-nosel)) args) (concat "\n" (make-string (+ 2 (length symbol)) ?\ )))) (propertize args 'face 'lispy-face-hint))))
(defun lispy--describe-clojure-java (sym) "Return description for Clojure Java symol SYM." (read (lispy--eval-clojure (format "(let [[_ cname mname] (re-find #\"(.*)/(.*)\" \"%s\")
methods (and cname (try (load-string (format \"(.getMethods %%s)\" cname)) (catch Exception e))) methods (filter #(= (.getName %%) mname) methods)] (if (= 0 (count methods)) nil (clojure.string/join \"\\n\" (map (fn [m] (.toString m)) methods))))"
sym))))
(defun lispy--clojure-macrop (symbol) "Test if SYMBOL is a macro." (equal (lispy--eval-clojure (format "(:macro (meta #'%s))" symbol)) "true"))
(defun lispy--clojure-middleware-unload () "Mark the Clojure middleware in \"lispy-clojure.clj\" as not loaded." (setq lispy--clojure-middleware-loaded-p nil))
(defvar cider-jdk-src-paths)
(defun lispy-cider-load-file (filename) (let ((ns-form (cider-ns-form))) (cider-map-repls :auto (lambda (connection) (when ns-form (cider-repl--cache-ns-form ns-form connection)) (cider-request:load-file (cider--file-string filename) (funcall cider-to-nrepl-filename-function (cider--server-filename filename)) (file-name-nondirectory filename) connection)))))
(defun lispy--clojure-middleware-load () "Load the custom Clojure code in \"lispy-clojure.clj\"." (unless lispy--clojure-middleware-loaded-p (setq lispy--clojure-ns "user") (save-window-excursion (lispy-cider-load-file (expand-file-name "lispy-clojure.clj" lispy-site-directory))) (setq lispy--clojure-middleware-loaded-p t) (add-hook 'nrepl-disconnected-hook #'lispy--clojure-middleware-unload) (let ((sources-expr (format "(do \n %s)" (mapconcat (lambda (p) (format "(cemerick.pomegranate/add-classpath %S)" p)) cider-jdk-src-paths "\n ")))) (lispy--eval-clojure sources-expr))))
(defun lispy-flatten--clojure (_arg) "Inline a Clojure function at the point of its call." (let* ((begp (if (looking-at lispy-left) t (if (lispy-right-p) (progn (backward-list) nil) (lispy-left 1)))) (bnd (lispy--bounds-list)) (str (lispy--string-dwim bnd)) (expr (lispy--read str)) (result (if (and (symbolp (car expr)) (lispy--clojure-macrop (symbol-name (car expr)))) (lispy--eval-clojure (format "(macroexpand '%s)" str)) (lispy--eval-clojure (format "(lispy-clojure/flatten-expr '%s)" str))))) (goto-char (car bnd)) (delete-region (car bnd) (cdr bnd)) (insert result) (when begp (goto-char (car bnd)))) (lispy-alt-multiline))
(defun lispy--clojure-debug-step-in () "Inline a Clojure function at the point of its call." (lispy--clojure-detect-ns) (let* ((e-str (format "(lispy-clojure/debug-step-in\n'%s)" (lispy--string-dwim))) (str (substring-no-properties (lispy--eval-clojure e-str)))) (lispy-follow) (when (string-match "(clojure.core/in-ns (quote \\([^)]+\\))" str) (setq lispy--clojure-ns (match-string 1 str))) (lispy--eval-clojure str) (lispy-flow 1)))
(defun lispy-goto-line (line) (goto-char (point-min)) (forward-line (1- line)))
(declare-function archive-zip-extract "arc-mode")
(defun lispy-find-archive (archive path) (require 'arc-mode) (let ((name (format "%s:%s" archive path))) (switch-to-buffer (or (find-buffer-visiting name) (with-current-buffer (generate-new-buffer name) (archive-zip-extract archive path) (set-visited-file-name name) (setq-local default-directory (file-name-directory archive)) (setq-local buffer-read-only t) (set-buffer-modified-p nil) (set-auto-mode) (current-buffer))))))
(defun lispy-goto-symbol-clojure (symbol) "Goto SYMBOL." (lispy--clojure-detect-ns) (let* ((r (read (lispy--eval-clojure (format "(lispy-clojure/location '%s)" symbol)))) (url (car r)) (line (cadr r)) archive) (cond ((file-exists-p url) (find-file url) (lispy-goto-line line)) ((and (string-match "\\`file:\\([^!]+\\)!/\\(.*\\)\\'" url) (file-exists-p (setq archive (match-string 1 url)))) (let ((path (match-string 2 url))) (lispy-find-archive archive path) (lispy-goto-line line))) (t (warn "unexpected: %S" symbol) (cider-find-var symbol)))))
(defun lispy-goto-symbol-clojurescript (symbol) "Goto SYMBOL." (cider-find-var nil symbol))
(defun lispy-clojure-complete-at-point () (when (car (cider-connections)) (ignore-errors (lispy--clojure-detect-ns) (let* ((lispy-ignore-whitespace t) (bnd (or (bounds-of-thing-at-point 'symbol) (cons (point) (point)))) (obj (cond ((save-excursion (lispy--out-backward 1) (looking-at "(\\.\\.")) (concat (buffer-substring-no-properties (match-beginning 0) (car bnd)) ")")) ((save-excursion (lispy--back-to-paren) (when (looking-at "(\\.[\t\n ]") (ignore-errors (forward-char 1) (forward-sexp 2) (lispy--string-dwim))))))) res) (cond ((and obj (setq res (lispy--eval-clojure (format "(lispy-clojure/object-members %s)" obj))) (null lispy--clojure-errorp)) (let ((cands (read res))) (when (> (cdr bnd) (car bnd)) (setq cands (all-completions (lispy--string-dwim bnd) cands))) (list (car bnd) (cdr bnd) cands))) ((save-excursion (lispy--out-backward 2) (looking-at "(import")) (let* ((prefix (save-excursion (lispy--out-backward 1) (forward-char) (thing-at-point 'symbol t))) (cands (read (lispy--eval-clojure (format "(lispy-clojure/complete %S)" prefix)))) (len (1+ (length prefix))) (candsa (mapcar (lambda (s) (substring s len)) cands))) (when (> (cdr bnd) (car bnd)) (setq candsa (all-completions (lispy--string-dwim bnd) candsa))) (list (car bnd) (cdr bnd) candsa))) (t (let* ((prefix (lispy--string-dwim bnd)) (cands (read (lispy--eval-clojure (format "(lispy-clojure/complete %S)" prefix))))) (list (car bnd) (cdr bnd) cands))))))))
(defun lispy--clojure-dot-args () (save-excursion (lispy--back-to-paren) (let* ((object (save-mark-and-excursion (lispy-mark-list 2) (lispy--string-dwim))) (method (save-mark-and-excursion (lispy-mark-list 3) (lispy--string-dwim))) (sig (read (lispy--eval-clojure (format "(lispy-clojure/method-signature (lispy-clojure/reval \"%s\" nil) \"%s\")" object method))))) (when (> (length sig) 0) (if (string-match "\\`public \\(.*\\)(\\(.*\\))\\'" sig) (let ((name (match-string 1 sig)) (args (match-string 2 sig))) (format "%s\n(. %s %s%s)" name object method (if (> (length args) 0) (concat " " args) ""))) sig)))))
(defun lispy--clojure-constructor-args (symbol) (read (lispy--eval-clojure (format "(lispy-clojure/ctor-args %s)" symbol))))
(defun lispy--clojure-pretty-string (str) "Return STR fontified in `clojure-mode'." (cond ((string-match "\\`\"error: \\([^\0]+\\)\"\\'" str) (concat (propertize "error: " 'face 'error) (match-string 1 str))) ((> (length str) 4000) str) (t (condition-case nil (with-temp-buffer (clojure-mode) (insert str) (lispy-font-lock-ensure) (buffer-string)) (error str)))))
(defun lispy-clojure-apropos-action (s) (cider-doc-lookup (substring (car (split-string s "\\\\n")) 2)))
(defun lispy-clojure-apropos () (interactive) (let ((cands (split-string (lispy--eval-clojure "(lispy-clojure/all-docs 'clojure.core)") "::"))) (ivy-read "var: " cands :action #'lispy-clojure-apropos-action)))
(provide 'le-clojure)
;;; le-clojure.el ends here
|