Emacs に TidalCycles のコード補完機能を追加する
Emacs で TidalCycles を使う場合、Atom などのエディタと違いデフォルトではコード補完機能[1]がない。lsp (haskell-language-server) を使えば動くのかもしれないが、TidalCycles の動く環境が GHCi のためか自分の環境ではうまくいかなかった。そこで Emacs にコード補完を提供する company-mode について少し調べてみると、思いの外シンプルだったので自作することにした。
使い方
上記リポジトリの company-tidal.el
を load-path
に追加し
(add-to-list 'company-backends 'company-tidal)
でcompany-backends
に追加すると使える。
company-mode について
まず company-mode の backend がどのように動いているかについてはこの記事が非常に参考になった(これを読まなかったら自分で書こうとは思わなかった)。
company-mode がどのようにコード補完を行っているのかはほぼこの記事に書かれているので、ここではそれを TidalCycles でどうやって使うのかを書く[2]。
まず backend のコード本体から。
※ 以下、記載するコードは実際に使っているコードの抜粋なので主要でない部分の説明は省く。
(defun company-tidal (command &optional arg &rest ignored)
"Company backend for TidalCycles"
(interactive (list 'interactive))
(cl-case command
(interactive (company-begin-backend 'company-tidal))
;; 初期化処理
;; 'tidal-send-string が存在しない場合は起動しない
(init (if (fboundp 'tidal-send-string)
(unless company-tidal--initialized
(add-hook 'company-completion-cancelled-hook 'company-tidal--completion-cancelled)
(with-current-buffer tidal-buffer
(add-hook 'comint-preoutput-filter-functions 'company-tidal--preoutput-filter))
(setq company-tidal--initialized t))
(error "no tidal process running?")))
(prefix (and (eq major-mode 'tidal-mode)
(company-grab-symbol)))
(candidates (cons :async
(lambda (callback)
(company-tidal--find-candidates arg callback))))
(annotation (cons :async
(lambda (callback)
(company-tidal--find-annotation arg callback))))
(post-completion (company-tidal--post-completion arg))
))
大まかな流れとしては init
で初期化処理したのち、
coandidates
で補完候補を、annotation
で型情報を非同期で取得し、
post-completion
で仕上げの処理をしている。
補完候補の取得
TidalCycles は Haskell の repl(GHCi)で動いていて、幸いにも GHCi は :complete
で補完機能を提供している。
例えばs
をいう文字で100件補完候補を取得する場合はこのように書く。
tidal> :complete repl 100 "s"
100 444 ""
"s"
"sBusses"
"sConfig"
"sCxs"
"sDefault"
"sGlobalFMV"
...以下省略
これをemacs-lispで実行すれば補完候補を取得できる。
(defun company-tidal--find-candidates (string callback)
"補完のリクエスト"
(company-tidal--create-request
(concat ":complete repl " (format "%d" company-tidal-candidates-limit) " \"" string "\"" "\n")
(lambda (output)
(let* ((outputs (split-string output "\n"))
(outputs-without-prompt (seq-remove (lambda (item) (string-match "tidal>" item)) outputs))
(outputs-without-quotes (seq-map (lambda (item) (string-trim item "\"" "\"")) outputs-without-prompt))
(outputs-without-duplicate (delq nil (delete-dups outputs-without-quotes)))
(result (seq-drop outputs-without-duplicate 1)))
(funcall callback result)))))
TidalCycles(GHCi)にコマンドを送る
tidal-mode で提供している tidal-send-string
でコマンドを送ることができる。
(defun company-tidal--create-request (req completion)
"tidal (comint) にリクエストを送る"
(setq company-tidal--processing t)
(setq company-tidal--request-completion completion)
(tidal-send-string req))
tidal-mode は comint-mode
でシェルを生成していて、tidal-send-string
の実態は comint-send-string
だ。これを add-hook でキャッチすれば出力値を取得できる。
初期化処理で add-hook (comint-preoutput-filter-functions
) を追加している。
(init (if (fboundp 'tidal-send-string)
(unless company-tidal--initialized
(add-hook 'company-completion-cancelled-hook 'company-tidal--completion-cancelled)
(with-current-buffer tidal-buffer
(add-hook 'comint-preoutput-filter-functions 'company-tidal--preoutput-filter))
(setq company-tidal--initialized t))
(error "no tidal process running?")))
これで company-tidal--preoutput-filter
の output
に出力値が入ってくるので、あとはこれをレスポンスとして返してやればよい。ここではリクエスト時に設定したクロージャ company-tidal--request-completion
にレスポンスを出力している。
(defun company-tidal--preoutput-filter (output)
"comint の出力フィルタ"
(cond
;; 補完のリクエストがある場合は出力をしない
(company-tidal--request-completion
(let* ((match-prompt (string-match "tidal>" output)))
;; output を company-tidal--output に結合
(setq company-tidal--output (concat company-tidal--output output))
;; "output に tidal> プロンプトがある場合、company-tidal--request-completion に出力"
(when (and match-prompt)
(let* ((output company-tidal--output)
(request-completion company-tidal--request-completion))
(funcall request-completion output)
(setq company-tidal--output nil)
(setq company-tidal--request-completion nil)
)))
"")
;; 処理中の場合は出力をしない
(company-tidal--processing "")
;; 通常の出力
(t output)))
戻り値として ""
を返しているのは、tidal-mode の出力フレームに大量の補完候補が流れてしまうからだ。preoutput を使うのはそれが理由で、ここで""
にフィルタリングしている。
型情報の取得
これだけでも補完としては充分だが、せっかくなのでannotation
に型情報も追加してみる。といっても GHCi で:type
を実行するだけで要領は同じだ。
(defun company-tidal--find-annotation (string callback)
"annotation リクエスト"
(company-tidal--create-request
(concat ":type (" string ")\n")
(lambda (output)
(let* ((outputs (split-string output "\n"))
(outputs-without-prompt (seq-remove (lambda (item) (string-match "tidal>" item)) outputs))
(outputs-without-nil (delq nil outputs-without-prompt))
(type-info (apply #'concatenate 'string outputs-without-nil))
;; :: で分けて関数名を削除
(type-info-split-name (split-string type-info "::"))
(type-info-without-name (seq-drop type-info-split-name 1))
(result (apply #'concatenate 'string type-info-without-name)))
(funcall callback (concat "::" result))))))
仕上げとキャンセル処理
tidal-mode の出力をフィルタリングしているフラグ company-tidal--processing
を nil
にして、通常の出力に戻している。
(defun company-tidal--finish ()
"終了処理"
(setq company-tidal--processing nil))
(defun company-tidal--post-completion (string)
"完了処理"
(company-tidal--finish))
(defun company-tidal--completion-cancelled (arg)
"キャンセル処理"
(company-tidal--finish))
完成
-
Atom では
:browse Sound.Tidal.Context
で初期化時に全ての補完情報を取得しているようだ。この手法のメリットとしては初期化が終われば高速に補完ができる。ただし Sound.Tidal.Context 上の関数しか補完ができないデメリットがある。今回は自作関数含め、全て補完するために都度:complete
で補完候補を取得する方法を取った。 ↩︎ -
TidalCycles というよりは、ほぼ GHCi の話なので、少し改造すれば汎用的に GHCi で使えるコードになる(はず...)。 ↩︎
Discussion