Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
403 lines (357 sloc) 15.9 KB
;;; swank-clojure-extra.el --- Some handy utilities for using
;; swank-clojure with slime.
;; Copyright (C) 2008, 2009, 2010 Jeffrey Chu,
;; Phil Hagelberg
;; Ramakrishnan Muthukrishnan
;; Juergen Hoetzel
;; Authors: Jeffrey Chu <>
;; Phil Hagelberg <>
;; Ramakrishnan Muthukrishnan <>
;; Juergen Hoetzel <>
;; URL:
;; Version: 1.1.0
;; Keywords: languages, lisp
;; This file is licensed under the terms of the GNU General Public
;; License as distributed with Emacs (press C-h C-c to view it).
;;; Commentary:
;; The purpose of this file is to set up `slime-lisp-implementations'
;; to allow SLIME to communicate with the Swank server implemented in
;; Clojure. There are mainly 3 ways to launch a session:
;; 1. Standalone: Configure the swank-clojure-classpath with the
;; paths of the jars you wish to use (this should include clojure
;; and swank-clojure at the minimum and may also include
;; clojure-contrib)and hit M-x slime.
;; 2. Project: Put your project's dependencies (either manually or using
;; Leiningen or Maven) in the directory named by
;; `swank-clojure-project-dep-path' (lib/ by default), then launch M-x
;; swank-clojure-project. Note that the directory must contain
;; swank-clojure in the classpath (either swank-clojure.jar or the
;; swank-clojure source repo), it will not automatically be added.
;; 3. Standalone Server: Users of leiningen or clojure-maven-plugin
;; can launch a server from a shell
;; (
;; and connect to it from within Emacs using M-x slime-connect.
;; `swank-clojure-lein-swank' can be used to start a leiningen
;; interactively from emacs.
;;; Code:
(require 'slime)
(require 'clojure-mode)
(defgroup swank-clojure nil
"SLIME/swank support for clojure"
:prefix "swank-clojure-"
:group 'applications)
(defcustom swank-clojure-java-path "java"
"The location of the java executable"
:type 'string
:group 'swank-clojure)
(defcustom swank-clojure-jar-home "~/.swank-clojure/"
"The directory where the jars necessary to run swank-clojure are kept."
:type 'string
:group 'swank-clojure)
(defun swank-clojure-default-classpath ()
(when (and (file-directory-p "~/.clojure")
(directory-files "~/.clojure" nil "swank-clojure.*jar$"))
(directory-files "~/.clojure" t ".jar$"))
(when (file-directory-p swank-clojure-jar-home)
(directory-files swank-clojure-jar-home t ".jar$"))))
(defcustom swank-clojure-classpath
"The classpath from which clojure will load from (passed into
java as the -cp argument). On default, it includes all jar files
within ~/.clojure/ and ~/.swank-clojure"
:type 'list
:group 'swank-clojure)
;; For backwards-compatibility:
(defcustom swank-clojure-extra-classpaths nil
"Extra project specific classpaths that can be added during load time."
:type 'list
:group 'swank-clojure)
(defcustom swank-clojure-library-paths nil
"The library paths used when loading shared libraries,
used to set the java.library.path property"
:type 'list
:group 'swank-clojure)
(defcustom swank-clojure-extra-vm-args nil
"Extra arguments to be passed to the Java VM when starting clojure.
For example -Xmx512m or -Dsun.java2d.noddraw=true"
:type 'list
:group 'swank-clojure)
(defcustom swank-clojure-binary nil
"Used as a binary executable (instead of swank-clojure-java-path) if non-nil."
:type 'string
:group 'swank-clojure)
(defcustom swank-clojure-init-files nil
"If provided, will be used to initialize the REPL environment."
:type 'list
:group 'swank-clojure)
(defcustom swank-clojure-compile-p nil
"Whether to instruct swank-clojure to compile files. Set to nil
if it's causing you problems."
:type 'boolean
:group 'swank-clojure)
(defcustom swank-clojure-project-dep-path "lib"
"The directory (relative to the project root) to look for dependencies in
when using `swank-clojure-project'."
:type 'string
:group 'swank-clojure)
(defcustom swank-clojure-deps
(list (concat ""
(concat ""
(concat ""
"A list of urls of jars required to run swank-clojure. If they
don't exist in `swank-clojure-jar-home' and
`swank-clojure-classpath' is not set, the user will be prompted
to download them when invoking `slime'.
Due to a bug in url-retrieve-synchronously, they must be
downloaded in order of size (ascending), so if you customize
this, keep that in mind."
:type 'list
:group 'swank-clojure)
(defcustom swank-clojure-lein-swank-command "lein"
"lein program file name. It is searched for in PATH. You can also
set an absolute path, if the Leiningen bin directory is not in your PATH
:type 'file
:group 'swank-clojure)
(defface swank-clojure-dim-trace-face
'((((class color) (background dark))
(:foreground "grey50"))
(((class color) (background light))
(:foreground "grey55")))
"Face used to dim parentheses."
:group 'slime-ui)
(defun swank-clojure-init (file encoding)
(when swank-clojure-compile-p
"(require 'swank.loader)\n\n(swank.loader/init)\n\n")
"(require 'swank.swank)\n\n"
(when (boundp 'slime-protocol-version)
(format "(swank.swank/ignore-protocol-version %S)\n\n"
;; Hacked in call to get the localhost address to work around a bug
;; where the REPL doesn't pop up until the user presses Enter.
"(do (.. getLocalHost getHostAddress) nil)"
(format "(swank.swank/start-server %S :encoding %S)\n\n"
(expand-file-name file)
(format "%s" (slime-coding-system-cl-name encoding)))))
(defun swank-clojure-find-package ()
(let ((regexp "^(\\(clojure.core/\\)?\\(in-\\)?ns\\+?[ \t\n\r]+\\(#\\^{[^}]+}[ \t\n\r]+\\)?[:']?\\([^()\" \t\n]+\\>\\)"))
(when (or (re-search-backward regexp nil t)
(re-search-forward regexp nil t))
(match-string-no-properties 4)))))
(defun swank-clojure-slime-mode-hook ()
(slime-mode 1)
(set (make-local-variable 'slime-find-buffer-package-function)
(defun swank-clojure-update-indentation (sym indent)
(put sym 'clojure-indent-function indent))
(defun swank-clojure-concat-paths (paths)
"Concatenate given list of `paths' using `path-separator'. (`expand-file-name'
will be used over paths too.)"
(mapconcat 'identity (mapcar 'expand-file-name paths) path-separator))
(defun swank-clojure-parse-jar-name (url)
(car (last (split-string url "/"))))
(defun swank-clojure-download-jar (url)
(let ((jar-name (swank-clojure-parse-jar-name url)))
(message "Downloading %s..." jar-name)
(let ((download-buffer (url-retrieve-synchronously url)))
(condition-case e
(set-buffer download-buffer)
(re-search-forward "HTTP/[0-9]\.[0-9] 200 OK")
(re-search-forward "^$" nil 'move)
(delete-region (point-min) (+ 1 (point)))
(write-file (concat swank-clojure-jar-home "/" jar-name))
(kill-buffer nil))
;; no recursive directory deletion on emacs 22 =(
(dolist (j (directory-files swank-clojure-jar-home t "[^.]+$"))
(delete-file j))
(delete-directory swank-clojure-jar-home)
(error "Failed to download Clojure jars.")))))))
(defun swank-clojure-dep-exists-p (jar-url)
"True if the jar file in `jar-url' exists in `swank-clojure-jar-home'."
(file-exists-p (expand-file-name (swank-clojure-parse-jar-name jar-url)
(defun swank-clojure-check-install ()
"Prompt to install Clojure if it's not already present."
(when (and (not swank-clojure-classpath)
(or (not (file-exists-p swank-clojure-jar-home))
(> (count-if-not 'swank-clojure-dep-exists-p swank-clojure-deps)
(y-or-n-p "It looks like Clojure is not installed. Install now? "))
(make-directory swank-clojure-jar-home t)
(dolist (j swank-clojure-deps)
(swank-clojure-download-jar j))
(setq swank-clojure-classpath (swank-clojure-default-classpath))))
(defun swank-clojure-cmd ()
"Create the command to start clojure according to current settings."
(if swank-clojure-binary
(if (listp swank-clojure-binary)
(list swank-clojure-binary))
(list swank-clojure-java-path)
(when swank-clojure-library-paths
(concat "-Djava.library.path="
(swank-clojure-concat-paths swank-clojure-library-paths)))
(swank-clojure-concat-paths (append swank-clojure-classpath
(let ((init-opts '()))
;; TODO: cleanup
(dolist (init-file swank-clojure-init-files init-opts)
(setq init-opts (append init-opts (list "-i" init-file))))
(list "--repl")))))
(defun swank-clojure-reset-implementation ()
"Redefines the clojure entry in `slime-lisp-implementations'."
(require 'assoc)
(aput 'slime-lisp-implementations 'clojure
(list (swank-clojure-cmd) :init 'swank-clojure-init)))
(defadvice slime-read-interactive-args (before add-clojure)
;; Unfortunately we need to construct our Clojure-launching command
;; at slime-launch time to reflect changes in the classpath. Slime
;; has no mechanism to support this, so we must resort to advice.
;; Change the repl to be more clojure friendly
(defun swank-clojure-slime-repl-modify-syntax ()
(when (string-match "\\*slime-repl clojure\\*" (buffer-name))
;; modify syntax
(set-syntax-table clojure-mode-syntax-table)
;; set indentation function (already local)
(setq lisp-indent-function 'clojure-indent-function)
;; set paredit keys
(when (and (featurep 'paredit) paredit-mode (>= paredit-version 21))
(define-key slime-repl-mode-map "{" 'paredit-open-curly)
(define-key slime-repl-mode-map "}" 'paredit-close-curly))))
;; Debugger
(defun swank-clojure-dim-font-lock ()
"Dim irrelevant lines in Clojure debugger buffers."
(if (string-match "clojure" (buffer-name))
nil `((,(concat " [0-9]+: " (regexp-opt '("clojure.core"
"swank." "java."))
;; TODO: regexes ending in .* are ignored by
;; font-lock; what gives?
. font-lock-comment-face)) t)))
(add-hook 'sldb-mode-hook 'swank-clojure-dim-font-lock)
(defvar swank-clojure-project-hook nil
"A hook to run when a new SLIME session starts via `swank-clojure-project'.
The `path' variable is bound to the project root when these functions run.")
(defun swank-clojure-javadoc (classname)
"Show the javadoc for classname using clojure.contrib.repl-utils/javadoc"
(interactive (list (read-from-minibuffer "Javadoc for: " (slime-sexp-at-point))))
,(concat "(try
(require 'clojure.contrib.repl-utils)
(@(ns-resolve 'clojure.contrib.repl-utils 'javadoc) " classname ")
(catch Throwable t (.getMessage t)))"))))
(defun directoryp (path)
"Return t is path is a directory or a symlink pointing to a directory."
(let ((first-attr (car (file-attributes path))))
(if (stringp first-attr)
(directoryp first-attr)
(defun swank-clojure-project (path)
"Setup classpath for a clojure project and starts a new SLIME session.
Kills existing SLIME session, if any."
(interactive (list
"Project root: "
(if (functionp 'locate-dominating-file) ; Emacs 23 only
(locate-dominating-file default-directory "src")
;; TODO: allow multiple SLIME sessions per Emacs instance
(when (get-buffer "*inferior-lisp*") (kill-buffer "*inferior-lisp*"))
(let ((slime-lisp-implementations (copy-list slime-lisp-implementations))
(swank-clojure-extra-vm-args (copy-list swank-clojure-extra-vm-args))
(swank-clojure-classpath (copy-list swank-clojure-classpath))
(swank-clojure-binary nil)
(swank-clojure-extra-classpaths (let ((l (expand-file-name
swank-clojure-project-dep-path path)))
(if (file-directory-p l)
(directory-files l t ".jar$")
(directory-files l t "^[^\\.]")))))))
(add-to-list 'swank-clojure-extra-classpaths (expand-file-name "classes/" path))
(add-to-list 'swank-clojure-extra-classpaths (expand-file-name "src/" path))
(add-to-list 'swank-clojure-extra-classpaths (expand-file-name "test/" path))
(add-to-list 'swank-clojure-extra-classpaths (expand-file-name "resources/" path))
;; For Maven style project layouts
(when (file-exists-p (expand-file-name "pom.xml" path))
(dolist (d '("src/main/clojure/" "src/test/clojure/"
"target/test-classes/" "target/classes/" "target/dependency/"))
(add-to-list 'swank-clojure-extra-classpaths (expand-file-name d path) t))
(dolist (d (let ((l (expand-file-name "target/dependency/" path)))
(if (file-directory-p l)
(directory-files l t ".jar$"))))
(add-to-list 'swank-clojure-extra-classpaths (expand-file-name d path) t))
(add-to-list 'swank-clojure-extra-vm-args
(format "-Dclojure.compile.path=%s"
(expand-file-name "target/classes/" path))))
(run-hooks 'swank-clojure-project-hook)
(let ((default-directory path))
(slime 'clojure)))))
(defun swank-clojure-lein-swank (directory)
"Start a lein swank process in directory (default `default-directory')"
(interactive (list (or
(locate-dominating-file default-directory "project.clj")
(read-directory-name "Leiningen Project directory: "))))
(let ((default-directory directory))
(when (not default-directory)
(error "Not in a Leiningen project."))
;; you can customize slime-port using .dir-locals.el
(let ((proc (start-process "lein-swank" nil swank-clojure-lein-swank-command "swank" (number-to-string slime-port))))
(when proc
(process-put proc :output nil)
(set-process-sentinel proc (lambda (proc event)
(message "%s%s: `%S'"
(process-get proc :output)
proc (replace-regexp-in-string "\n" "" event))))
(set-process-filter proc
(lambda (proc output)
;; record last line of output until connected (possible error message)
(process-put proc :output (concat (process-get proc :output) output))
(when (string-match "Connection opened on" output)
(slime-connect "localhost" slime-port)
;; no need to further process output
(set-process-filter proc nil))))
(message "Starting swank server...")))))
(provide 'swank-clojure-extra)
;;; swank-clojure-extra.el ends here