jsx-mode.el に補完機能を実装しました

先日密かに MELPA に登録した jsx-mode.el ですが、develop ブランチのものに補完機能を実装しました。
先に断っておくと、jsx の –complete オプションを使って実装していますが、重すぎて使いものになりません。

セットアップ

リポジトリを clone して、develop ブランチであることを確認した上で src/jsx-mode.el を load-path の通った場所に移動します。

$ git clone git@github.com:jsx/jsx-mode.el.git
$ cd jsx-mode.el
$ git branch
* develop

init.el に次の内容を記述して jsx-use-auto-complete と t にします。jsx-cmd を “jsx-with-server” に設定していますが、これによって補完にかかる時間が 0.2 秒ほど短くなります。

(custom-set-variables
 '(jsx-cmd "jsx-with-server")
 '(jsx-use-auto-complete t))

使ってみる

例えば次のようなコードにおいて、3行目13列目にカーソルがある状態で M-x auto-complete を実行してみると・・・

class _Main {
    static function main(args : string) : void {
        "".s
    }
}

次のような結果になります。ドキュメントは ac-quick-help によって表示されます。
20130909034555

ただし、デフォルトの設定だと2文字以上のトークンを入力すると自動補完機能が働いてしまいプチフリが頻発するので次のような設定を入れておくといいかもしれません。
100文字以上のトークンを入力すると自動補完機能が発動するという設定で、実質無効化です。全モード共通で無効化するなら ac-auto-start をいじってください。

(defun jsx-mode-init ()
  (nconc jsx-ac-source '((requires . 100))))

(add-hook 'jsx-mode-hook 'jsx-mode-init)

とにかく重い!

補完候補を出すのに、先ほどの小さなファイルに対して、jsx コマンドで 0.3 秒以上、jsx-with-server で 0.1 秒以上かかります。ハイエンドなマシンならもっと速いでしょうが。

$ time jsx --complete 3:13 complete.jsx > /dev/null

real	0m0.349s
user	0m0.298s
sys	0m0.050s

$ time jsx-with-server --complete 3:13 complete.jsx > /dev/null

real	0m0.138s
user	0m0.079s
sys	0m0.013s

js/web.jsx をインポートしようものなら jsx-with-server であっても 0.35 秒程度かかります。
自動補完をオンにしたらカクカクして話になりません。

import "js/web/jsx"

class _Main {
    static function main(args : string) : void {
        "".s
    }
}
$ time jsx --complete 5:13 complete.jsx > /dev/null

real	0m0.346s
user	0m0.296s
sys	0m0.050s

大規模開発に適用しようとすると数秒待たされます。1
この辺は @wasabiz が改善してくれることでしょう。

ちょっとした工夫点

メジャーモード側であまり定義したくなかったんですが、次のような advice を定義しています。

(defadvice auto-complete (before jsx--add-requires-to-ac-source activate)
  "Invoke completion whenever auto-complete is executed."
  (if (string= major-mode "jsx-mode")
      (add-to-list 'jsx-ac-source '(requires . 0))))

(defadvice auto-complete (after jsx--remove-requires-from-ac-source activate)
  (if (string= major-mode "jsx-mode")
      (setq jsx-ac-source (delete '(requires . 0) jsx-ac-source))))

(defadvice fill-region (before jsx--fill-region activate)
  "Preserve the line feeds in documents
cf. https://github.com/auto-complete/popup-el/issues/43"
  (when jsx--try-to-show-document-p
    (beginning-of-buffer)
    (replace-string "\n" jsx--hard-line-feed)
    (setq use-hard-newlines t)))

auto-complete に関しては、明示的に auto-complete を呼び出した場合はトークンの長さが 0 であっても補完候補を出すために定義しています。
jsx-auto-complete のような関数を定義しても良いかもしれませんが、設定なしで auto-complete と同じキーバインドを使えるということから advice を使うことにしました。

fill-region に関してはここの問題を回避するために定義しています。

おまけ ~個人的な設定~

使えそうな設定を晒しておきます。ご自由にお使いください。

(autoload 'jsx-mode "jsx-mode" nil t)
(add-to-list 'auto-mode-alist '("\\.jsx'" . jsx-mode))

;; flymake
;; flymake は現在のスクリプトに対するエラーメッセージなしでエラー終了すると異常終了する
(defadvice flymake-post-syntax-check (before flymake-force-check-was-interrupted activate)
  (setq flymake-check-was-interrupted t))
(setq jsx-use-flymake t)
;; シンタックスチェックを parse ではなく compile にする(より厳密なチェック)
(setq jsx-syntax-check-mode "compile")

(defun jsx-imenu-create-index ()
  (let (index)
    (goto-char (point-min))
    (while (re-search-forward
            (concat
             "^\\s-*\\(?:override\\s-+\\)?\\(?:static\\s-+\\)?function\\s-+\\("
             jsx--identifier-re
             ".+?\\)\\s-*\\(?:{\\|$\\)")
            (point-max) t)
      (push (cons (match-string 1) (match-beginning 1)) index))
    (nreverse index)))

(defun jsx-mode-init ()
  (setq jsx-cmd-options (list "--add-search-path" (expand-file-name "/path/to/project")))
  ;; import "foo/*.jsx" みたいな書き方をしていると lock file もシンタックスチェックの対象になってしまう(設定は自己責任で)
  (add-hook 'post-command-hook 'unlock-buffer)
  ;; flymake のエラーメッセージを popup で表示する
  (define-key jsx-mode-map (kbd "C-c d") 'jsx-display-popup-err-for-current-line)
  ;; cf. https://github.com/jsx/jsx-mode.el/issues/2
  (define-key jsx-mode-map (kbd "}")
    (lambda (arg)
      (interactive "*P")
      (self-insert-command (prefix-numeric-value arg))
      (indent-according-to-mode)))
  ;; ざっくりとした imenu 対応
  (setq imenu-create-index-function 'jsx-imenu-create-index)
  ;; auto-complete(jsx の補完機能は使わない)
  (when (require 'auto-complete nil t)
    (add-to-list 'ac-modes 'jsx-mode)))
(add-hook 'jsx-mode-hook 'jsx-mode-init)
  1. なので、どうせ実装しても使いものにならないってことで補完機能の実装のモチベーションがありませんでした