TSServerの使い方メモ


はじめに

最近, TypeScript 1.5関連のエントリが少しずつ上がってきてるけど, このエントリはその中で最も誰得?となること間違いなし!

最初に断っておくが、このエントリを読んで得するのは、これから何かしらのエディタ(EclipseとかEmacsとか秀丸とか自分の信じている神に従え)でTypeScript向けのPluginを作ろうと思っている人限定である。
「他人の作ったpluginなんて使う気にすらならないぜ!自分で実装するぜ!」って奴は, こんなもん読まなくても自分で何とかしそうな気がプンプンする.

さて, 先日 別のエントリにて記載したが, TypeScript v1.5.0 alpha版公開に合わせて, TSServerを利用したVim plugin tsuquyomiを作成&公開した.

このエントリでは, tsuquyomiを作成する上で身についたTSServerの基礎知識を、備忘録も兼ねて書き連ねていこうと思う.

TSServerとは

TSServerはTypeScript 1.5からbundleされたエディタ向けのサーバであり, tsserver コマンドで動作する.
本家のwiki にも記載がある通り, TSServerはLanguage ServiceにエディタやIDEがアクセスするためのアダプタの位置づけである.

詳細な機能は後述するが, TSServerを使うと下記機能をエディタから利用可能となる:

  • 編集しているソースのcompile error取得
  • ClassやInterfaceの定義情報取得
  • リファクタリング(識別子の名称変更)
  • 補完候補の取得
  • etc...

特徴的なのは, APIのインターフェイスに標準入出力を採用しているため, エディタ/IDEの側ではプロセスとSTDIN/STDOUTを扱う仕組みさえあればよく, Node.jsによる実装は一切必要ないという親切設計な点.

TSServerを利用した各種Plugin

TSServerの使い方

メッセージの書式

Requestは, 次のようなJSONフォーマットで組み立ててTSServerの標準入力に流し込む形となる.

{"type": "request", "seq": 0, "command": "open", "arguments": {"file": "sample1.ts"}}
  • type : "request" 固定. 省略しても動く.
  • seq : リクエストのシーケンス番号. クライアント側で適宜インクリメントして利用する. 省略しても動く.
  • command : requestの種別. 利用可能な文字列は後述.
  • arguments : commandの引数. command毎に異なる.

実行例

うだうだ書くより実例で示した方が早いのでサンプルをば.

まず, TSServerに読み込ませるサンプルのソースコードを作成する.

sample1.ts
module SampleModule {
  export interface ISome {
    name: string;
  }

  export class SomeClass implements ISome {
    constructor(public name: string) {}
  }
}

次に, TSServerの命令を用意する(インタラクティブにやってもいいけど,面倒なのでファイルにしておく)

definition.json
{"type": "request", "seq": 0, "command": "open", "arguments": {"file": "sample1.ts"}}
{"type": "request", "seq": 1, "command": "definition", "arguments": {"file": "sample1.ts", "line":6, "offset":37}}

内容である程度察しが付くと思うが, 上記のcommandは下記を表している.

  1. open command で先に用意したsample1.tsを開かせ,
  2. definition commandで sample1.ts上の6行37列目に書いてある ISomeの定義箇所を取得.

これをtsserverで実行すると,

tsserver < definition.json

下記の標準出力が得られる.

Content-Length: 171

{"seq":0,"type":"response","command":"definition","request_seq":1,"success":true,"body":[{"file":"sample1.ts","start":{"line":2,"offset":3},"end":{"line":4,"offset":4}}]}

interface ISomeの定義は, sample1.tsファイルの2 行3 列目〜4行4列目にありまっせ、と正しく定義箇所情報が返却された.

APIs

TypeScriptレポジトリのsrc/server/session.tsを読むと一覧が載っているが, TSServerに用意されたcommandは以下の通り:

command type 機能概要
brace response ファイルのカーソルから見て, brace matchingする箇所を返す({に対応する}の場所等). 一般的なエディタであれば, デフォルトで持っている機能であり, いらない子.
change none ファイルの変更箇所をTSServerに通達する.
close none TSServerでファイルを閉じる.
completions response ファイルのカーソル(lineとoffsetで指定)位置における補完可能な単語の一覧を取得.
completionEntryDetails response completions commandで取得した補完情報の詳細を取得.
configure none インデント時のtab幅等, フォーマットに必要な設定を行う. tsconfigやcompilerOptions的な内容ではないので注意.
definition response ファイルのカーソル上のsymbolについて, そのsymbolの定義箇所を取得する.
exit none tsserver自体を終了する.
format response ソースをフォーマットする?使ってないのでよくわからん
formatonkey response これも使ってないのでよく分からん.
geterr event コンパイルエラー情報を取得する. 現状, 唯一のevent発火API(後述).
navbar response ファイルの見出しを作成する. IDEのサイドメニューに表示するような一覧情報を作るのに便利.
navto response キーワード検索を行う.
occurrences response カーソル上のIdentiferの出現箇所を取得する. referencesと似ているが, referencesは「どのようなcontextで参照しているか」まで取得可能であるのに対し, occurrencesは出現している場所情報のみを返却する.
open none ファイルをTSServer上で開く.
quickinfo response ファイルのカーソル上の情報を取得.
references response ファイルのカーソル上のsymbolについて, そのsymbolを参照している箇所を既にTSServerでOpenしたファイルから検索し, 参照箇所の一覧を返す.
reload none ファイルをリロードする. change と違い, 新ファイルで旧ファイルの内容を全置き換えするイメージ.
rename response ファイルのカーソル上に存在するsymbol(identifier)を別名に置き換える場合に, 同時に置換すべき箇所の一覧を返す. リファクタリング用の機能を作るのに便利.
saveto none デバッグ用command. TSServerが認識しているfileの内容をダンプする. reload, change commandで, TSServer上のファイル内容を変更しまくった時, 「あれ, TSServerが認識しているファイル内容ってどないやねん」ってなったときに利用する.
signatureHelp response ファイルカーソル上のメソッド呼び出しについて, 引数情報等の詳細を取得.

typeに"none"と記載のあるcommandについては, commandを実行しても何のresponseも出力されない. 上手く処理されているかどうかしりたければ, ログ(後述)に頼るしかない.

各commandのarguements, response bodyについては, src/protocol.d.ts を見れば完全な情報が記載されているので参考にされたし.

実用時のcommand flow

実際にエディタ向けのpluginを作成する場合は, 下記のようなフローに従う.

  1. TSServerのプロセスを立ち上げる.
  2. oepn commandで対象の.tsファイルをTSServer上で開く.
  3. definitionやcompletions commandでTSSserverから情報を引っこ抜く.
  4. エディタ側で対象の.tsファイルを編集したら, reload or change commandで変更内容をTSServerに通達.
  5. 編集が完全に完了したらclose comanndでファイルを閉じる.
  6. (エディタ終了時等で)TSServerのプロセスを落とす.

諸注意

ファイルパス

上記の例では, 簡便のために相対パスで記載していたが, "arguments: {"file": "..."}} については, 絶対パスで指定すること .
また, windowsで使う場合は, \\/に置き換えておくこと.

tsconfig.json

open command実行時に, 対象ファイルのbaseディレクトリからrootに向かって, tsconfig.jsonを探索する.
このときにtsconfig.jsonが見つかれば, compilerOptionsが適用される.
当然, geterr commandの結果に影響してくる.
自分でtsconfig.jsonをopenしても意味はないし, reloadやchangeコマンドの実行時は探索されない.

したがって, open commandの後に tsconfig.jsonを変更した場合, TSServerにtsconfigの変更を通知する術が現状存在しないため, close & openを実行するしかない.

geterr

geterr commandは結果が非同期で返ってくる.
従って, 先のdefinition.jsonをリダイレクトで食わせるような実行をしても, eventがemitされる前にprocessが落ちてしまう.
また, このcommandは1リクエストに対して, "synatxDeagnostic"(文法エラー情報)と"semanticsDegnostic"(セマンティックエラー情報)の二つのイベントを発火させるため, plugin側で両方のイベント(標準出力)を監視する必要がある.

困った時は

1.5.0-alphaがリリースされる前からtsuquyomiの開発を始めていたというのもあるが, APIが思う通りの結果を返してくれず、うんうん悩む時間も多かった.
困ったらログとデバッグですよねー.

ログ

process.env.TSS_LOG でTSServerにログを吐かせることが出来る.

TSS_LOGの設定例
export TSS_LOG="-file `pwd`/tsserver.log -level verbose"

-file で出力先ファイルの指定, -levelで出力レベルの指定が行える.

デバッグ

ログでも追えないときはnode-inspectorを使ってブレークポイント仕掛けていけば何とかなる.

まとめ

  • TSServer使ってイカしたIDEプラグイン作ろうぜ. VS使いに見せつけてやろうぜ