Personal emacs config
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.

584 lines
21 KiB

  1. ;;; le-clojure.el --- lispy support for Clojure. -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2014-2018 Oleh Krehel
  3. ;; This file is not part of GNU Emacs
  4. ;; This file is free software; you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation; either version 3, or (at your option)
  7. ;; any later version.
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;; For a full copy of the GNU General Public License
  13. ;; see <http://www.gnu.org/licenses/>.
  14. ;;; Commentary:
  15. ;;
  16. ;;; Code:
  17. ;;* Requires
  18. (require 'lispy)
  19. (require 'cider-client nil t)
  20. (require 'cider-connection nil t)
  21. (require 'cider-eval nil t)
  22. (require 'cider-find nil t)
  23. (defcustom lispy-clojure-eval-method 'cider
  24. "REPL used for eval."
  25. :type '(choice
  26. (const :tag "CIDER" cider)
  27. (const :tag "UNREPL" spiral))
  28. :group 'lispy)
  29. (defcustom lispy-cider-connect-method 'cider-jack-in
  30. "Function used to create a CIDER connection."
  31. :type '(choice
  32. (const cider-jack-in)
  33. (const cider-connect)
  34. (function :tag "Custom"))
  35. :group 'lispy)
  36. ;;* Namespace
  37. (defvar lispy--clojure-ns "user"
  38. "Store the last evaluated *ns*.")
  39. (defun lispy--clojure-detect-ns ()
  40. "When there's only one (ns ...) in the buffer, use it."
  41. (save-excursion
  42. (goto-char (point-min))
  43. (when (re-search-forward clojure-namespace-name-regex nil t)
  44. (let ((ns (match-string-no-properties 4)))
  45. (when (not (re-search-forward clojure-namespace-name-regex nil t))
  46. (setq lispy--clojure-ns ns))))))
  47. ;;* User wrapper for eval
  48. (defvar lispy--clojure-middleware-loaded-p nil
  49. "Nil if the Clojure middleware in \"lispy-clojure.clj\" wasn't loaded yet.")
  50. (defun lispy-eval-clojure (e-str)
  51. "User facing eval."
  52. (lispy--clojure-detect-ns)
  53. (let (c-str)
  54. (unless (stringp e-str)
  55. (setq e-str (lispy--string-dwim))
  56. (setq c-str (let ((deactivate-mark nil)
  57. (lispy-ignore-whitespace t))
  58. (save-mark-and-excursion
  59. (lispy--out-backward 1)
  60. (deactivate-mark)
  61. (lispy--string-dwim)))))
  62. (let ((f-str
  63. (cond
  64. ((eq major-mode 'clojurescript-mode)
  65. e-str)
  66. (lispy--clojure-middleware-loaded-p
  67. (format (if (eq this-command 'special-lispy-eval)
  68. "(lispy-clojure/pp (lispy-clojure/reval %S %S :file %S :line %S))"
  69. "(lispy-clojure/reval %S %S :file %S :line %S)")
  70. e-str c-str (buffer-file-name) (line-number-at-pos)))
  71. (t
  72. e-str))))
  73. (cond ((eq current-prefix-arg 7)
  74. (kill-new f-str))
  75. ((and (eq current-prefix-arg 0)
  76. (lispy--eval-clojure
  77. "(lispy-clojure/shadow-unmap *ns*)")
  78. nil))
  79. ((eq lispy-clojure-eval-method 'spiral)
  80. (lispy--eval-clojure-spiral e-str))
  81. (t
  82. (lispy--eval-clojure f-str e-str))))))
  83. ;;* Start REPL wrapper for eval
  84. (defvar lispy--clojure-hook-lambda nil
  85. "Store a lambda to call.")
  86. (defun lispy--clojure-eval-hook-lambda ()
  87. "Call `lispy--clojure-hook-lambda'."
  88. (when lispy--clojure-hook-lambda
  89. (funcall lispy--clojure-hook-lambda)
  90. (setq lispy--clojure-hook-lambda nil))
  91. (remove-hook 'nrepl-connected-hook
  92. 'lispy--clojure-eval-hook-lambda))
  93. (eval-after-load 'cider
  94. '(progn
  95. (cider-add-to-alist 'cider-jack-in-dependencies
  96. "org.tcrawley/dynapath" "0.2.5")
  97. (cider-add-to-alist 'cider-jack-in-dependencies
  98. "com.cemerick/pomegranate" "0.4.0")
  99. (cider-add-to-alist 'cider-jack-in-dependencies
  100. "compliment" "0.3.6")
  101. (cider-add-to-alist 'cider-jack-in-dependencies
  102. "me.raynes/fs" "1.4.6")))
  103. (declare-function cider-connections "ext:cider-connection")
  104. (defun lispy--eval-clojure (str &optional add-output)
  105. "Eval STR as Clojure code.
  106. The result is a string.
  107. When ADD-OUTPUT is non-nil, add the standard output to the result."
  108. (require 'cider)
  109. (unless (eq major-mode 'clojurescript-mode)
  110. (add-hook 'cider-connected-hook #'lispy--clojure-middleware-load))
  111. (let (deactivate-mark)
  112. (if (null (car (cider-connections)))
  113. (progn
  114. (setq lispy--clojure-hook-lambda
  115. `(lambda ()
  116. (set-window-configuration
  117. ,(current-window-configuration))
  118. (message
  119. (lispy--eval-clojure-1 ,str ,add-output))))
  120. (add-hook 'nrepl-connected-hook
  121. 'lispy--clojure-eval-hook-lambda t)
  122. (call-interactively lispy-cider-connect-method)
  123. (format "Starting CIDER using %s ..." lispy-cider-connect-method))
  124. (unless lispy--clojure-middleware-loaded-p
  125. (lispy--clojure-middleware-load))
  126. (lispy--eval-clojure-1 str add-output))))
  127. ;;* Base eval
  128. (defvar lispy--clojure-errorp nil)
  129. (defun lispy--eval-clojure-1 (str add-output)
  130. (setq lispy--clojure-errorp nil)
  131. (or
  132. (and (stringp add-output)
  133. (lispy--eval-clojure-handle-ns add-output))
  134. (let* (pp
  135. (stra (if (setq pp (string-match "\\`(lispy-clojure/\\(pp\\|reval\\)" str))
  136. str
  137. (format "(do %s)" str)))
  138. (res (lispy--eval-nrepl-clojure stra lispy--clojure-ns))
  139. (status (nrepl-dict-get res "status"))
  140. (res (cond ((or (member "namespace-not-found" status))
  141. (lispy--eval-nrepl-clojure stra))
  142. ((member "eval-error" status)
  143. (setq lispy--clojure-errorp t)
  144. res)
  145. (t
  146. res)))
  147. (val
  148. (nrepl-dict-get res "value"))
  149. out)
  150. (cond ((null val)
  151. (lispy--clojure-pretty-string
  152. (nrepl-dict-get res "err")))
  153. (add-output
  154. (concat
  155. (if (setq out (nrepl-dict-get res "out"))
  156. (concat (propertize out 'face 'font-lock-string-face) "\n")
  157. "")
  158. (lispy--clojure-pretty-string
  159. (if pp
  160. (condition-case nil
  161. (string-trim (read val))
  162. (error val))
  163. val))))
  164. (t
  165. (lispy--clojure-pretty-string val))))))
  166. (defun lispy--eval-clojure-handle-ns (str)
  167. (when (or (string-match "\\`(ns \\([a-z-_0-9\\.]+\\)" str)
  168. (string-match "\\`(in-ns '\\([a-z-_0-9\\.]+\\)" str))
  169. (setq lispy--clojure-ns (match-string 1 str))
  170. (let* ((res (lispy--eval-nrepl-clojure str "user"))
  171. (status (nrepl-dict-get res "status")))
  172. (when (member "eval-error" status)
  173. (error (nrepl-dict-get res "err"))))
  174. lispy--clojure-ns))
  175. ;;* Handle NREPL version incompat
  176. (defun lispy--eval-nrepl-clojure (str &optional namespace)
  177. (nrepl-sync-request:eval
  178. str
  179. (cider-current-connection)
  180. namespace))
  181. (defvar spiral-conn-id)
  182. (defvar spiral-aux-sync-request-timeout)
  183. (declare-function spiral-projects-as-list "ext:spiral-project")
  184. (declare-function spiral-pending-eval-add "ext:spiral-project")
  185. (declare-function spiral-ast-unparse-to-string "ext:spiral-ast")
  186. (declare-function spiral-loop--send "ext:spiral-loop")
  187. (defun lispy--eval-clojure-spiral (str)
  188. (let* ((start (current-time))
  189. (repl-buf (cdr (assoc :repl-buffer (car (spiral-projects-as-list)))))
  190. (conn-id (with-current-buffer repl-buf spiral-conn-id))
  191. (unparse-no-properties
  192. (lambda (node) (substring-no-properties
  193. (spiral-ast-unparse-to-string node))))
  194. stdout
  195. result)
  196. (spiral-loop--send conn-id :aux str)
  197. (spiral-pending-eval-add
  198. :aux conn-id
  199. :status :sent
  200. :eval-callback (lambda (eval-payload)
  201. (setq result (funcall unparse-no-properties eval-payload)))
  202. :stdout-callback (lambda (stdout-payload &rest _)
  203. (setq stdout
  204. (concat stdout
  205. (funcall unparse-no-properties stdout-payload)))))
  206. (while (and (not result)
  207. (not (input-pending-p)) ;; do not hang UI
  208. (or (not spiral-aux-sync-request-timeout)
  209. (< (cadr (time-subtract (current-time) start))
  210. spiral-aux-sync-request-timeout)))
  211. (accept-process-output nil 0.01))
  212. (if stdout
  213. (concat stdout "\n" result)
  214. result)))
  215. ;;* Rest
  216. (defvar cider--debug-mode-response)
  217. (declare-function cider--debug-mode "ext:cider-debug")
  218. (defvar nrepl-ongoing-sync-request)
  219. (defun lispy--clojure-debug-quit ()
  220. (let ((pt (save-excursion
  221. (if (lispy--leftp)
  222. (forward-list)
  223. (lispy--out-forward 1))
  224. (lispy-up 1)
  225. (point)))
  226. (str (format "(do %s)"
  227. (mapconcat
  228. (lambda (x)
  229. (format "(def %s %s)" (car x) (cadr x)))
  230. (nrepl-dict-get cider--debug-mode-response "locals")
  231. "\n"))))
  232. (nrepl-send-request
  233. (list "op" "debug-input" "input" ":quit"
  234. "key" (nrepl-dict-get cider--debug-mode-response "key"))
  235. (lambda (_response))
  236. (cider-current-connection))
  237. (lispy--eval-clojure str)
  238. (ignore-errors
  239. (let ((nrepl-ongoing-sync-request nil))
  240. (cider--debug-mode -1)))
  241. (goto-char pt)))
  242. (defun lispy--clojure-resolve (symbol)
  243. "Return resolved SYMBOL.
  244. Return 'special or 'keyword appropriately.
  245. Otherwise try to resolve in current namespace first.
  246. If it doesn't work, try to resolve in all available namespaces."
  247. (let ((str (lispy--eval-clojure
  248. (format "(lispy-clojure/resolve-sym '%s)" symbol))))
  249. (cond
  250. (lispy--clojure-errorp
  251. (user-error str))
  252. ((string-match "^#'\\(.*\\)$" str)
  253. (match-string 1 str))
  254. (t
  255. (read str)))))
  256. (defun lispy--clojure-symbol-to-args (symbol)
  257. (cond ((string= symbol ".")
  258. (lispy--clojure-dot-args))
  259. ((string-match "\\`\\(.*\\)\\.\\'" symbol)
  260. (lispy--clojure-constructor-args (match-string 1 symbol)))
  261. (t
  262. (let ((sym (lispy--clojure-resolve symbol)))
  263. (cond
  264. ((eq sym 'special)
  265. (read
  266. (lispy--eval-clojure
  267. (format "(lispy-clojure/arglist '%s)" symbol))))
  268. ((eq sym 'keyword)
  269. (list "[map]"))
  270. ((eq sym 'undefined)
  271. (error "Undefined"))
  272. ((and (listp sym) (eq (car sym) 'variable))
  273. (list "variable"))
  274. (t
  275. (read
  276. (lispy--eval-clojure
  277. (format "(lispy-clojure/arglist '%s)" symbol)))))))))
  278. (defun lispy--clojure-args (symbol)
  279. "Return a pretty string with arguments for SYMBOL.
  280. Besides functions, handles specials, keywords, maps, vectors and sets."
  281. (let ((args (lispy--clojure-symbol-to-args symbol)))
  282. (if (listp args)
  283. (format
  284. "(%s %s)"
  285. (propertize symbol 'face 'lispy-face-hint)
  286. (mapconcat
  287. #'identity
  288. (mapcar (lambda (x) (propertize (downcase x)
  289. 'face 'lispy-face-req-nosel))
  290. args)
  291. (concat "\n"
  292. (make-string (+ 2 (length symbol)) ?\ ))))
  293. (propertize args 'face 'lispy-face-hint))))
  294. (defun lispy--describe-clojure-java (sym)
  295. "Return description for Clojure Java symol SYM."
  296. (read
  297. (lispy--eval-clojure
  298. (format
  299. "(let [[_ cname mname] (re-find #\"(.*)/(.*)\" \"%s\")
  300. methods (and cname
  301. (try (load-string (format \"(.getMethods %%s)\" cname))
  302. (catch Exception e)))
  303. methods (filter #(= (.getName %%) mname) methods)]
  304. (if (= 0 (count methods))
  305. nil
  306. (clojure.string/join
  307. \"\\n\" (map (fn [m] (.toString m))
  308. methods))))"
  309. sym))))
  310. (defun lispy--clojure-macrop (symbol)
  311. "Test if SYMBOL is a macro."
  312. (equal (lispy--eval-clojure
  313. (format "(:macro (meta #'%s))" symbol))
  314. "true"))
  315. (defun lispy--clojure-middleware-unload ()
  316. "Mark the Clojure middleware in \"lispy-clojure.clj\" as not loaded."
  317. (setq lispy--clojure-middleware-loaded-p nil))
  318. (defvar cider-jdk-src-paths)
  319. (defun lispy-cider-load-file (filename)
  320. (let ((ns-form (cider-ns-form)))
  321. (cider-map-repls :auto
  322. (lambda (connection)
  323. (when ns-form
  324. (cider-repl--cache-ns-form ns-form connection))
  325. (cider-request:load-file (cider--file-string filename)
  326. (funcall cider-to-nrepl-filename-function
  327. (cider--server-filename filename))
  328. (file-name-nondirectory filename)
  329. connection)))))
  330. (defun lispy--clojure-middleware-load ()
  331. "Load the custom Clojure code in \"lispy-clojure.clj\"."
  332. (unless lispy--clojure-middleware-loaded-p
  333. (setq lispy--clojure-ns "user")
  334. (save-window-excursion
  335. (lispy-cider-load-file
  336. (expand-file-name "lispy-clojure.clj" lispy-site-directory)))
  337. (setq lispy--clojure-middleware-loaded-p t)
  338. (add-hook 'nrepl-disconnected-hook #'lispy--clojure-middleware-unload)
  339. (let ((sources-expr
  340. (format
  341. "(do \n %s)"
  342. (mapconcat
  343. (lambda (p) (format "(cemerick.pomegranate/add-classpath %S)" p))
  344. cider-jdk-src-paths
  345. "\n "))))
  346. (lispy--eval-clojure sources-expr))))
  347. (defun lispy-flatten--clojure (_arg)
  348. "Inline a Clojure function at the point of its call."
  349. (let* ((begp (if (looking-at lispy-left)
  350. t
  351. (if (lispy-right-p)
  352. (progn (backward-list)
  353. nil)
  354. (lispy-left 1))))
  355. (bnd (lispy--bounds-list))
  356. (str (lispy--string-dwim bnd))
  357. (expr (lispy--read str))
  358. (result
  359. (if (and (symbolp (car expr))
  360. (lispy--clojure-macrop (symbol-name (car expr))))
  361. (lispy--eval-clojure
  362. (format "(macroexpand '%s)" str))
  363. (lispy--eval-clojure
  364. (format "(lispy-clojure/flatten-expr '%s)" str)))))
  365. (goto-char (car bnd))
  366. (delete-region (car bnd) (cdr bnd))
  367. (insert result)
  368. (when begp
  369. (goto-char (car bnd))))
  370. (lispy-alt-multiline))
  371. (defun lispy--clojure-debug-step-in ()
  372. "Inline a Clojure function at the point of its call."
  373. (lispy--clojure-detect-ns)
  374. (let* ((e-str (format "(lispy-clojure/debug-step-in\n'%s)"
  375. (lispy--string-dwim)))
  376. (str (substring-no-properties
  377. (lispy--eval-clojure e-str))))
  378. (lispy-follow)
  379. (when (string-match "(clojure.core/in-ns (quote \\([^)]+\\))" str)
  380. (setq lispy--clojure-ns (match-string 1 str)))
  381. (lispy--eval-clojure str)
  382. (lispy-flow 1)))
  383. (defun lispy-goto-line (line)
  384. (goto-char (point-min))
  385. (forward-line (1- line)))
  386. (declare-function archive-zip-extract "arc-mode")
  387. (defun lispy-find-archive (archive path)
  388. (require 'arc-mode)
  389. (let ((name (format "%s:%s" archive path)))
  390. (switch-to-buffer
  391. (or (find-buffer-visiting name)
  392. (with-current-buffer (generate-new-buffer name)
  393. (archive-zip-extract archive path)
  394. (set-visited-file-name name)
  395. (setq-local default-directory (file-name-directory archive))
  396. (setq-local buffer-read-only t)
  397. (set-buffer-modified-p nil)
  398. (set-auto-mode)
  399. (current-buffer))))))
  400. (defun lispy-goto-symbol-clojure (symbol)
  401. "Goto SYMBOL."
  402. (lispy--clojure-detect-ns)
  403. (let* ((r (read (lispy--eval-clojure
  404. (format "(lispy-clojure/location '%s)" symbol))))
  405. (url (car r))
  406. (line (cadr r))
  407. archive)
  408. (cond
  409. ((file-exists-p url)
  410. (find-file url)
  411. (lispy-goto-line line))
  412. ((and (string-match "\\`file:\\([^!]+\\)!/\\(.*\\)\\'" url)
  413. (file-exists-p (setq archive (match-string 1 url))))
  414. (let ((path (match-string 2 url)))
  415. (lispy-find-archive archive path)
  416. (lispy-goto-line line)))
  417. (t
  418. (warn "unexpected: %S" symbol)
  419. (cider-find-var symbol)))))
  420. (defun lispy-goto-symbol-clojurescript (symbol)
  421. "Goto SYMBOL."
  422. (cider-find-var nil symbol))
  423. (defun lispy-clojure-complete-at-point ()
  424. (when (car (cider-connections))
  425. (ignore-errors
  426. (lispy--clojure-detect-ns)
  427. (let* ((lispy-ignore-whitespace t)
  428. (bnd (or (bounds-of-thing-at-point 'symbol)
  429. (cons (point) (point))))
  430. (obj (cond
  431. ((save-excursion
  432. (lispy--out-backward 1)
  433. (looking-at "(\\.\\."))
  434. (concat
  435. (buffer-substring-no-properties (match-beginning 0) (car bnd))
  436. ")"))
  437. ((save-excursion
  438. (lispy--back-to-paren)
  439. (when (looking-at "(\\.[\t\n ]")
  440. (ignore-errors
  441. (forward-char 1)
  442. (forward-sexp 2)
  443. (lispy--string-dwim)))))))
  444. res)
  445. (cond ((and obj
  446. (setq res (lispy--eval-clojure
  447. (format "(lispy-clojure/object-members %s)" obj)))
  448. (null lispy--clojure-errorp))
  449. (let ((cands (read res)))
  450. (when (> (cdr bnd) (car bnd))
  451. (setq cands (all-completions (lispy--string-dwim bnd) cands)))
  452. (list (car bnd) (cdr bnd) cands)))
  453. ((save-excursion
  454. (lispy--out-backward 2)
  455. (looking-at "(import"))
  456. (let* ((prefix (save-excursion
  457. (lispy--out-backward 1)
  458. (forward-char)
  459. (thing-at-point 'symbol t)))
  460. (cands (read (lispy--eval-clojure
  461. (format
  462. "(lispy-clojure/complete %S)"
  463. prefix))))
  464. (len (1+ (length prefix)))
  465. (candsa (mapcar (lambda (s) (substring s len)) cands)))
  466. (when (> (cdr bnd) (car bnd))
  467. (setq candsa (all-completions (lispy--string-dwim bnd) candsa)))
  468. (list (car bnd) (cdr bnd) candsa)))
  469. (t
  470. (let* ((prefix (lispy--string-dwim bnd))
  471. (cands (read (lispy--eval-clojure
  472. (format
  473. "(lispy-clojure/complete %S)"
  474. prefix)))))
  475. (list (car bnd) (cdr bnd) cands))))))))
  476. (defun lispy--clojure-dot-args ()
  477. (save-excursion
  478. (lispy--back-to-paren)
  479. (let* ((object (save-mark-and-excursion
  480. (lispy-mark-list 2)
  481. (lispy--string-dwim)))
  482. (method (save-mark-and-excursion
  483. (lispy-mark-list 3)
  484. (lispy--string-dwim)))
  485. (sig (read
  486. (lispy--eval-clojure
  487. (format "(lispy-clojure/method-signature (lispy-clojure/reval \"%s\" nil) \"%s\")" object method)))))
  488. (when (> (length sig) 0)
  489. (if (string-match "\\`public \\(.*\\)(\\(.*\\))\\'" sig)
  490. (let ((name (match-string 1 sig))
  491. (args (match-string 2 sig)))
  492. (format "%s\n(. %s %s%s)"
  493. name object method
  494. (if (> (length args) 0)
  495. (concat " " args)
  496. "")))
  497. sig)))))
  498. (defun lispy--clojure-constructor-args (symbol)
  499. (read (lispy--eval-clojure
  500. (format "(lispy-clojure/ctor-args %s)" symbol))))
  501. (defun lispy--clojure-pretty-string (str)
  502. "Return STR fontified in `clojure-mode'."
  503. (cond ((string-match "\\`\"error: \\([^\0]+\\)\"\\'" str)
  504. (concat (propertize "error: " 'face 'error)
  505. (match-string 1 str)))
  506. ((> (length str) 4000)
  507. str)
  508. (t
  509. (condition-case nil
  510. (with-temp-buffer
  511. (clojure-mode)
  512. (insert str)
  513. (lispy-font-lock-ensure)
  514. (buffer-string))
  515. (error str)))))
  516. (defun lispy-clojure-apropos-action (s)
  517. (cider-doc-lookup
  518. (substring
  519. (car (split-string s "\\\\n"))
  520. 2)))
  521. (defun lispy-clojure-apropos ()
  522. (interactive)
  523. (let ((cands
  524. (split-string (lispy--eval-clojure
  525. "(lispy-clojure/all-docs 'clojure.core)")
  526. "::")))
  527. (ivy-read "var: " cands
  528. :action #'lispy-clojure-apropos-action)))
  529. (provide 'le-clojure)
  530. ;;; le-clojure.el ends here