Permalink
Fetching contributors…
Cannot retrieve contributors at this time
300 lines (252 sloc) 10.3 KB
;;; ac-nrepl.el --- auto-complete sources for Clojure using nrepl completions
;; Copyright (C) 2012-2014 Steve Purcell <steve@sanityinc.com>
;; Author: Steve Purcell <steve@sanityinc.com>
;; Sam Aaron <samaaron@gmail.com>
;; URL: https://github.com/purcell/ac-nrepl
;; Keywords: languages, clojure, nrepl
;; Version: DEV
;; Package-Requires: ((cider "0.1") (auto-complete "1.4"))
;; 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/>.
;;; Commentary:
;; Provides a number of auto-complete sources for Clojure projects
;; using nrepl.
;;; Installation:
;; Available as a package in both Melpa (recommended) at
;; http://melpa.org/ and Marmalade at http://marmalade-repo.org/
;; M-x package-install ac-nrepl
;;; Usage:
;; (require 'ac-nrepl)
;; (add-hook 'cider-repl-mode-hook 'ac-nrepl-setup)
;; (add-hook 'cider-mode-hook 'ac-nrepl-setup)
;; (eval-after-load "auto-complete"
;; '(add-to-list 'ac-modes 'cider-repl-mode))
;; If you want to trigger auto-complete using TAB in nrepl buffers, you may
;; want to use auto-complete in your `completion-at-point-functions':
;; (defun set-auto-complete-as-completion-at-point-function ()
;; (setq completion-at-point-functions '(auto-complete)))
;; (add-hook 'auto-complete-mode-hook 'set-auto-complete-as-completion-at-point-function)
;;
;; (add-hook 'cider-repl-mode-hook 'set-auto-complete-as-completion-at-point-function)
;; (add-hook 'cider-mode-hook 'set-auto-complete-as-completion-at-point-function)
;;
;; You might consider using ac-nrepl's popup documentation in place of `nrepl-doc':
;;
;; (eval-after-load 'cider
;; '(define-key cider-mode-map (kbd "C-c C-d") 'ac-nrepl-popup-doc))
;;; Code:
(message "Notice: You probably want to use ac-cider instead of ac-nrepl.")
(require 'nrepl-client)
(require 'cider-interaction)
(require 'auto-complete)
(defun ac-nrepl-available-p ()
"Return t if nrepl is available for completion, otherwise nil."
(condition-case nil
(not (null (nrepl-current-tooling-session)))
(error nil)))
(defun ac-nrepl-sync-eval (clj)
"Synchronously evaluate CLJ.
Result is a plist, as returned from `nrepl-sync-request:eval'."
(nrepl-sync-request:eval clj (cider-current-ns) (nrepl-current-tooling-session)))
(defun ac-nrepl-candidates* (clj)
"Return completion candidates produced by evaluating CLJ."
(let ((response (plist-get (ac-nrepl-sync-eval (concat "(require 'complete.core) " clj))
:value)))
(when response
(car (read-from-string response)))))
(defun ac-nrepl-unfiltered-clj (clj)
"Return a version of CLJ with the completion prefix inserted."
(format clj ac-prefix))
(defun ac-nrepl-filtered-clj (clj)
"Build an expression which extracts the prefixed values from CLJ."
(concat "(filter #(.startsWith % \"" ac-prefix "\")"
(ac-nrepl-unfiltered-clj clj) ")"))
(defun ac-nrepl-candidates-ns ()
"Return namespace candidates."
(ac-nrepl-candidates*
(ac-nrepl-filtered-clj "(complete.core/namespaces *ns*)")))
(defun ac-nrepl-candidates-vars ()
"Return var candidates."
(ac-nrepl-candidates*
(ac-nrepl-filtered-clj "(let [prefix \"%s\"]
(if-not (.contains prefix \"/\")
(complete.core/ns-vars *ns*)
(let [ns-alias (symbol (first (.split prefix \"/\")))
core (find-ns 'clojure.core)]
(if-let [ns (or (get (ns-aliases *ns*) ns-alias)
(find-ns ns-alias))]
(let [vars (complete.core/ns-vars ns)
vars (if (= core ns)
vars
(remove (into #{} (complete.core/ns-vars core)) vars))]
(map (fn [x] (str ns-alias \"/\" x)) vars))
'()))))")))
(defun ac-nrepl-candidates-ns-classes ()
"Return namespaced class candidates."
(ac-nrepl-candidates*
(ac-nrepl-filtered-clj "(complete.core/ns-classes *ns*)")))
(defvar ac-nrepl-all-classes-cache nil
"Cached list of all classes loaded in the JVM backend.")
(defun ac-nrepl-refresh-class-cache ()
"Clear `ac-nrepl-all-classes-cache' and then refill it asynchronously."
(setq ac-nrepl-all-classes-cache nil)
(nrepl-request:eval
(concat "(require 'complete.core)"
(ac-nrepl-unfiltered-clj "(concat @complete.core/nested-classes
@complete.core/top-level-classes)"))
(nrepl-make-response-handler
(nrepl-current-connection-buffer)
(lambda (buffer value)
(setq ac-nrepl-all-classes-cache (car (read-from-string value))))
nil nil nil)
(cider-current-ns)
(nrepl-current-tooling-session)))
(add-hook 'nrepl-connected-hook 'ac-nrepl-refresh-class-cache t)
(defun ac-nrepl-candidates-all-classes ()
"Return java method candidates."
(when (string-match-p "^[a-zA-Z]+[a-zA-Z0-9$_]*\\.[a-zA-Z0-9$_.]*$" ac-prefix)
ac-nrepl-all-classes-cache))
(defun ac-nrepl-candidates-java-methods ()
"Return java method candidates."
(mapcar (lambda (hit)
(let* ((parts (split-string hit "#"))
(meth (nth 0 parts))
(classname (nth 1 parts)))
(propertize meth 'summary classname)))
(ac-nrepl-candidates*
(ac-nrepl-filtered-clj
"(for [class (vals (ns-imports *ns*))
method (.getMethods class)
:when (not (java.lang.reflect.Modifier/isStatic (.getModifiers method)))]
(str \".\" (.getName method) \"#\" (.getName class)))"))))
(defun ac-nrepl-candidates-static-methods ()
"Return static method candidates."
(ac-nrepl-candidates*
(ac-nrepl-filtered-clj
"(let [prefix \"%s\"]
(if (or (not (.contains prefix \"/\"))
(.startsWith prefix \"/\"))
'()
(let [scope (symbol (first (.split prefix \"/\")))]
(map (fn [memb] (str scope \"/\" memb))
(when-let [class (try (complete.core/resolve-class scope)
(catch java.lang.ClassNotFoundException e nil))]
(complete.core/static-members class)))))) ")))
(defun ac-nrepl-documentation (symbol)
"Return documentation for the given SYMBOL, if available."
(let ((raw-doc (plist-get (ac-nrepl-sync-eval
(format "(try (eval '(clojure.repl/doc %s))
(catch Exception e nil))"
symbol))
:stdout)))
(when raw-doc
(let ((doc
(substring-no-properties
(replace-regexp-in-string
"\r" ""
(replace-regexp-in-string
"^\\( \\|-------------------------\r?\n\\)" ""
raw-doc)))))
(unless (string-match "\\`[ \t\n]*\\'" doc)
doc)))))
(defun ac-nrepl-symbol-start-pos ()
"Find the starting position of the symbol at point, unless inside a string."
(let ((sap (symbol-at-point)))
(when (and sap (not (in-string-p)))
(car (bounds-of-thing-at-point 'symbol)))))
;;;###autoload
(defface ac-nrepl-candidate-face
'((t (:inherit ac-candidate-face)))
"Face for nrepl candidates."
:group 'auto-complete)
;;;###autoload
(defface ac-nrepl-selection-face
'((t (:inherit ac-selection-face)))
"Face for the nrepl selected candidate."
:group 'auto-complete)
;;;###autoload
(defconst ac-nrepl-source-defaults
'((available . ac-nrepl-available-p)
(candidate-face . ac-nrepl-candidate-face)
(selection-face . ac-nrepl-selection-face)
(prefix . ac-nrepl-symbol-start-pos)
(document . ac-nrepl-documentation))
"Defaults common to the various completion sources.")
;;;###autoload
(defvar ac-source-nrepl-ns
(append
'((candidates . ac-nrepl-candidates-ns)
(symbol . "n"))
ac-nrepl-source-defaults)
"Auto-complete source for nrepl ns completion.")
;;;###autoload
(defvar ac-source-nrepl-vars
(append
'((candidates . ac-nrepl-candidates-vars)
(symbol . "v"))
ac-nrepl-source-defaults)
"Auto-complete source for nrepl var completion.")
;;;###autoload
(defvar ac-source-nrepl-ns-classes
(append
'((candidates . ac-nrepl-candidates-ns-classes)
(symbol . "c"))
ac-nrepl-source-defaults)
"Auto-complete source for nrepl ns-specific class completion.")
;;;###autoload
(defvar ac-source-nrepl-all-classes
(append
'((candidates . ac-nrepl-candidates-all-classes)
(symbol . "c"))
ac-nrepl-source-defaults)
"Auto-complete source for nrepl all class completion.")
;;;###autoload
(defvar ac-source-nrepl-java-methods
(append
'((candidates . ac-nrepl-candidates-java-methods)
(symbol . "m"))
ac-nrepl-source-defaults)
"Auto-complete source for nrepl java method completion.")
;;;###autoload
(defvar ac-source-nrepl-static-methods
(append
'((candidates . ac-nrepl-candidates-static-methods)
(symbol . "s"))
ac-nrepl-source-defaults)
"Auto-complete source for nrepl java static method completion.")
;;;###autoload
(defun ac-nrepl-setup ()
"Add the nrepl completion source to the front of `ac-sources'.
This affects only the current buffer."
(interactive)
(add-to-list 'ac-sources 'ac-source-nrepl-ns)
(add-to-list 'ac-sources 'ac-source-nrepl-vars)
(add-to-list 'ac-sources 'ac-source-nrepl-ns-classes)
(add-to-list 'ac-sources 'ac-source-nrepl-all-classes)
(add-to-list 'ac-sources 'ac-source-nrepl-java-methods)
(add-to-list 'ac-sources 'ac-source-nrepl-static-methods))
;;;###autoload
(defun ac-nrepl-popup-doc ()
"A popup alternative to `nrepl-doc'."
(interactive)
(let ((doc (ac-nrepl-documentation (symbol-at-point))))
(when doc
(popup-tip doc
:point (ac-nrepl-symbol-start-pos)
:around t
:scroll-bar t
:margin t))))
(provide 'ac-nrepl)
;; Local Variables:
;; coding: utf-8
;; eval: (checkdoc-minor-mode 1)
;; End:
;;; ac-nrepl.el ends here