TSServerの使い方メモ


# はじめに

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

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

さて, 先日 [別のエントリ](http://qiita.com/Quramy/items/e0f004695e8bcda7604e)にて記載したが, TypeScript v1.5.0 alpha版公開に合わせて, [TSServerを利用したVim plugin tsuquyomi](https://github.com/Quramy/tsuquyomi)を作成&公開した.

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

# TSServerとは

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

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

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

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

## TSServerを利用した各種Plugin

* [VisualStudio Code](https://code.visualstudio.com/) : TypeScriptの連携機能にはバンドルされているTSServerが利用されている. MicroSoft謹製.
* [TypeScript-Sublime-Plugin](https://github.com/Microsoft/TypeScript-Sublime-Plugin) : Sublime向けplugin. MicroSoft謹製.
* [tsuquyomi](https://github.com/Quramy/tsuquyomi) : 拙作Vim plugin. 


# TSServerの使い方

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

```json
{"type": "request", "seq": 0, "command": "open", "arguments": {"file": "sample1.ts"}}
```

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

## 実行例

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

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

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

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

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

```json: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を開かせ,
1. definition commandで sample1.ts上の6行37列目に書いてある `ISome`の定義箇所を取得.

これをtsserverで実行すると,

```bash
tsserver < definition.json
```

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

```text
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](https://github.com/Microsoft/TypeScript/blob/master/src/server/session.ts#L78)を読むと一覧が載っているが, 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](https://github.com/Microsoft/TypeScript/blob/master/src/server/protocol.d.ts) を見れば完全な情報が記載されているので参考にされたし.

## 実用時のcommand flow

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

1. TSServerのプロセスを立ち上げる.
1. oepn commandで対象の.tsファイルをTSServer上で開く.
1. definitionやcompletions commandでTSSserverから情報を引っこ抜く.
1. エディタ側で対象の.tsファイルを編集したら, reload or change commandで変更内容をTSServerに通達.
1. 編集が完全に完了したらclose comanndでファイルを閉じる.
1. (エディタ終了時等で)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にログを吐かせることが出来る.

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

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

## デバッグ
ログでも追えないときは[node-inspector](https://github.com/node-inspector/node-inspector)を使ってブレークポイント仕掛けていけば何とかなる.

# まとめ

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