読者です 読者をやめる 読者になる 読者になる

(define -ayalog '())

括弧に魅せられて道を外した名前のないプログラマ

Clojure の開発環境をよりシンプルへ ~ inf-clojure 実践的な設定編?

Clojure Emacs 開発

一ヶ月くらい前に inf-clojure 導入記事を書いたのに、それ以来何も触れてなかったのでそろそろまた書いてみる。

一ヶ月くらい前に書いたってことはつまりあれから一ヶ月くらいは使っているわけですが、間に少し Cursive を使っていたのでまるまるじゃないですが、だいたい仕事中は inf-clojure 使っています。まぁ不満点はほぼほぼないです。

さくっとコードの全貌を置いておきます。

(add-to-list 'auto-mode-alist '("\\.boot$" . clojure-mode))
(add-to-list 'auto-mode-alist '("\\.cljs$'" . clojure-mode))
(add-to-list 'auto-mode-alist '("\\.cljx$'" . clojure-mode))
(add-to-list 'auto-mode-alist '("\\.cljc$'" . clojure-mode))
(use-package clojure-mode
  :defer t
  :config
  (progn
    (use-package smart-newline-mode)
    (use-package clojure-mode-extra-font-locking)
    (use-package align-cljlet
      :init (bind-keys :map clojure-mode-map
                       ("C-c j a l" . align-cljlet)))
    (use-package midje-mode)
    (use-package clj-refactor
      :config (cljr-add-keybindings-with-prefix "C-c j"))
    (use-package clojure-snippets)
    (use-package inf-clojure
      :config
      (progn
        (setq inf-clojure-prompt-read-only nil)
        (setq inf-clojure-program "boot repl")

        (defun my/inf-clojure-refresh ()
          (interactive)
          (inf-clojure-eval-string
           "(require '[clojure.tools.namespace.repl])
            (apply clojure.tools.namespace.repl/set-refresh-dirs (get-env :directories))
            (clojure.tools.namespace.repl/refresh)"))

        (defun my/find-tag-without-ns (tag)
          (interactive
           (list (my/helm-gtags--read-tagname 'tag-without-ns)))
          (helm-gtags--common '(helm-source-gtags-tags) tag))

        (defun my/run-clojure ()
          (interactive)
          (setq current-prefix-arg '(4))
          (call-interactively 'run-clojure)
          (paredit-mode))

        (defun my/in-ns-boot-home ()
          (interactive)
          (inf-clojure-eval-string "(in-ns 'boot.user)"))

        (bind-keys :map inf-clojure-minor-mode-map
                   ("C-c C-x" . my/inf-clojure-refresh)
                   ("C-c z" . my/run-clojure)
                   ("C-c C-z" . inf-clojure-switch-to-repl)
                   ("C-c C-h" . my/in-ns-boot-home)
                   ("M-." . my/find-tag-without-ns))))

    (defun my/clojure-mode-hook ()
      ;; (add-hook 'before-save-hook 'my/cleanup-buffer nil t)
      (clj-refactor-mode 1)
      (inf-clojure-minor-mode 1)
      (paredit-mode 1)
      (rainbow-delimiters-mode 1)
      (smart-newline-mode 1)
      (helm-gtags-mode 1))

    (add-hook 'clojure-mode-hook 'my/clojure-mode-hook)))

あと僕の設定ファイル晒しておくので適当に参照して参考にしてもらえればと。
ayato_p / dotemacs-for-clojure — Bitbucket

解説

inf-clojure の REPL がデフォルトで read-only なのでそれを消しています。

inf-clojure の REPL 起動コマンドを普段使う boot へと設定

REPL の reload をしています。 Cider ならデフォルトでありますが、 inf-clojure にはないのでこんな感じのコードを入れてあげると Cider 同様にツーストロークで reload 出来る。
ちなみにこれは for Boot なので Leiningen だとまた書き方が違うけど、だいたい似たようなコマンドはあった方が便利。

  • (defun my/find-tag-without-ns (tag)

helm-gtags の helm-gtags-find-tag を微修正した自分用 find-tag 。 Clojure のネームスペースに対して require :as した場合に 例えば (clojure.string :as str) とかした場合 str/replace とか書くんだけど、デフォルトの helm-gtags-find-tag を使うとカーソル位置の名前をそのまま引っ張ろうとして str/replace がデフォルト値になるのであまり便利じゃない。だから str/ の部分を削除出来るように自分用書いた。ちなみにこれのコードの途中にある (my/helm-gtags--read-tagname 'tag-without-ns) はこんな風に別のとこに定義してある。

    (add-to-list 'helm-gtags--prompt-alist '(tag-without-ns . "Find Definition: "))

    (defun my/helm-gtags--read-tagname (type &optional default-tagname)
      (let ((tagname (helm-gtags--token-at-point type))
            (prompt (assoc-default type helm-gtags--prompt-alist))
            (comp-func (assoc-default type helm-gtags-comp-func-alist)))
        (if (and tagname helm-gtags-use-input-at-cursor)
            tagname
          (when (and (not tagname) default-tagname)
            (setq tagname default-tagname))
          (when (eq type 'tag-without-ns)
            (setq tagname (first (last (split-string tagname "/")))))
          (when tagname
            (setq prompt (format "%s(default \"%s\") " prompt tagname)))
          (let ((completion-ignore-case helm-gtags-ignore-case)
                (completing-read-function 'completing-read-default))
            (completing-read prompt comp-func nil nil nil
                             'helm-gtags--completing-history tagname)))))

これは boot repl で起動したくないとき(例えば boot repl -c とか)したいときの為のコマンド。開発中は Boot の REPL サーバー立ち上げてやっていることが多いのでこれが必要。

  • (defun my/in-ns-boot-home ()

これは REPL のネームスペースが変更されているときに boot のデフォルトネームスペースに戻すために使う。理由としては boot のデフォルトネームスペースだと require しているコマンドとかが使えるので、結構頻繁にこれは使っている。

あとはだいたいおまけ。 auto-complate も他の場所で定義していますが、たぶん ac-cider がそのまま入っているので、ソースはそれと gtags とかが主に使われていますね。

            (setq-default ac-sources '(ac-source-yasnippet
                                       ac-source-abbrev
                                       ac-source-dictionary
                                       ac-source-words-in-same-mode-buffers
                                       ac-source-gtags))

clj-refactor も入っていますが nREPL は使っていないので、基本的には require とか import 書くときの便利なとこだけ利用しているというところです。

inf-clojure でも Cider 同様に出来ること

  • コード補完
  • 定義ジャンプ
  • REPL の利用

gtags を利用すればコードの補完や定義ジャンプは基本的にそんなに困らない。例えば Boot や Leiningen でライブラリをチェックアウトするタイミングで、それらのライブラリを解析して gtags を同様に作成出来て、それも定義タグのソースとして利用出来るようにすれば cider と同様の力を手に入れることが出来るとは思うんだけど、まぁコストが高いのでそこまでするつもりはない(せめて Clojure のコアライブラリくらいはやろうかな?という気持ちはあるけど、 inf-clojure でそもそもソースとかドキュメントは引っ張れるのであまり気にしていない)。

inf-clojure では Cider に及ばない部分

  • nREPL を利用した IDE ばりのコード補完
  • nREPL を利用した IDE ばりの定義ジャンプ
  • nREPL を利用した IDE ばりのリファクタリング機能(リネームとか)

個人的にはライブラリの中にまで飛びたいという気持ちは薄いのと補完は gtags レベルでもだいたい十分満足するレベルで使えるのでいいやという気持ち。あとリファクタリング機能はなんだかんだでそんなにいらないという気持ちが強くて理由は幾つかあるけど、リネームくらいなら helm-swoop で代用が効くしなーというのもある(老害脳かも…)。

Cider でも出来ないこと

  • Java ライブラリのソースをダウンロードして定義ジャンプ
  • デバッガのステップイン
  • map 形式のコードを綺麗にフォーマットする

Cursive は出来たけど、この辺は完全に IDE に負けているなぁというところだけど、デバッガなくても REPL あるからあんまり困ること少ない。

まとめ

主に nREPL 関係を使っていないことによる弱さがある。けど、ある程度までなら Cider で使っていたような機能は使えるから困っていない。あと macroexpand-all とかが Cider のようにデフォルトでないけど、これも自分で関数書けばいいだけなんで問題ないです(最近は覚えちゃって clojure.walk/macroexpand-all とか REPL で書いて実行したりすることもたまに)。なので、まぁ Cider 使ってるけど使いこなせてないとか、 cider-nrepl 重いし使いたくないとかいう人は inf-clojure 使ってみてもいいんじゃないんでしょうか。まぁ個人的に誰にでも勧めれるものではないと理解していますが*1、一定数いるであろう Cider を不満に思っているけど代替がない…と思っている人向けに書いてみました。まる。

余談

完全にこの話と関係ないけど、 id:syohex さんが inf-clojure ってまんま同じようなコンセプトのものを昔書いてて笑った。d.hatena.ne.jp

*1:Cider 便利だし