Language Server Index Format (LSIF)
2019年2月19日 Dirk Bäumer
チェックアウトなしでリッチなコードナビゲーション
開発者として、多くの時間をコードの読解やレビューに費やし、必ずしも新しいソースコードを作成するわけではありません。例えば、GitHubのようなリポジトリで既存のコードベースを参照したい場合や、同僚のプルリクエストをレビューしたい場合があるでしょう。
通常、ブランチをチェックアウトしたり、リポジトリをクローンしてソースコードをローカルマシンにダウンロードしたり、お好みの開発ツールを開いたりして、ようやくコードを読み、ナビゲートすることができます。最初にリポジトリをクローンすることなくこれができたら素晴らしいと思いませんか?ソースコードをダウンロードすることなく、ホバー情報、定義へ移動、すべての参照の検索のようなインテリジェントなコード機能を利用できることを想像してみてください。ブログ記事「リッチなコードナビゲーション体験の初見」では、プルリクエストのレビューにおけるこのシナリオを説明しています。
Language Server Index Format (LSIF、「else if」のように発音) の目標は、開発ツールやWeb UIで、ソースコードのローカルコピーを必要とせずに、リッチなコードナビゲーションをサポートすることです。このフォーマットは、リッチなコード編集機能を開発ツールに統合することを簡素化するLanguage Server Protocol (LSP) と精神的に類似しています。
既存のLSP言語サーバーを単に利用しないのはなぜでしょうか?LSPは、オートコンプリート、入力時のフォーマット、リッチなコードナビゲーションなどの豊富なコード作成機能を提供します。これらの機能を効率的に提供するためには、言語サーバーはすべてのソースコードファイルがローカルディスクで利用可能である必要があります。LSP言語サーバーは、ファイルの一部またはすべてをメモリに読み込み、抽象構文木を計算してこれらの機能を動かすこともあります。Language Server Index Formatの目標は、これらの要件なしにリッチなコードナビゲーション機能をサポートするためにLSPプロトコルを拡張することです。LSIFは、言語サーバーやその他のプログラミングツールがコードワークスペースに関する知識を出力するための標準フォーマットを定義します。この永続化された情報は、後で言語サーバーを実行することなく、同じワークスペースに対するLSPリクエストに応答するために使用できます。
言語サーバーインデックス形式
LSIFはLSPに基づいて構築されており、LSPで定義されているのと同じデータ型を使用します。大まかに言えば、LSIFは言語サーバーリクエストから返されるデータをモデル化します。LSPと同様に、LSIFはプログラムのシンボル情報を含まず、シンボルセマンティクス(例えば、シンボルの定義を構成するもの、またはあるメソッドが別のメソッドをオーバーライドするかどうか)も定義しません。したがって、LSIFはシンボルデータベースを定義せず、これはLSPのアプローチと一致しています。
既存のLSPデータ型をLSIFの基盤として使用することには、別の利点があります。LSPを既に理解しているツールやサーバーにLSIFを容易に統合できるためです。
例を見てみましょう。以下に示す内容のsample.ts
という名前のシンプルなTypeScriptファイルから始めます。
function bar(): void {}
bar()
にホバーすると、Visual Studio Codeで以下のホバー情報が表示されます。
このホバー情報は、LSPではHover
型を使用して表現されます。
export interface Hover {
/**
* The hover's content
*/
contents: MarkupContent | MarkedString | MarkedString[];
/**
* An optional range
*/
range?: Range;
}
上記の例では、具体的な値は以下の通りです。
{
contents: [{ language: 'typescript', value: 'function bar(): void' }];
}
クライアントツールは、ドキュメントfile:///Users/username/sample.ts
の{line: 0, character: 10}
の位置に対してtextDocument/hover
リクエストを送信することで、言語サーバーからホバーコンテンツを取得します。
LSIFは、言語サーバーまたはスタンドアロンツールが出力するフォーマットを定義し、タプル['textDocument/hover', 'file:///Users/username/sample.ts', {line: 0, character: 10}]
が上記のホバーに解決されることを記述します。このデータは取得され、データベースに永続化できます。
LSPリクエストは位置ベースですが、結果は単一の位置ではなく、範囲に対してのみ異なることがよくあります。上記のホバーの例では、bar
識別子のすべての位置でホバー値は同じです。これは、ユーザーがbar
のb
にホバーしても、bar
のr
にホバーしても、同じホバー値が返されることを意味します。出力されるデータをよりコンパクトにするために、LSIFは位置の代わりに範囲を使用します。この例では、LSIFツールは範囲情報を含むタプル['textDocument/hover', 'file:///Users/username/sample.ts', { start: { line: 0, character: 9 }, end: { line: 0, character: 12 }]
を出力します。
LSIFはこの情報を出力するためにグラフを使用します。グラフでは、LSPリクエストはエッジを使用して表現されます。ドキュメント、範囲、またはリクエストの結果(例えば、ホバー)は頂点を使用して表現されます。このフォーマットには以下の利点があります。
- 与えられたコード範囲に対して、異なる結果が存在しえます。特定の識別子範囲の場合、ユーザーはホバー値、定義の場所、またはすべての参照の検索に関心があります。したがって、LSIFはこれらの結果を範囲とリンクします。
- 新しいエッジまたは頂点の種類を追加することで、追加のリクエストタイプや結果でフォーマットを簡単に拡張できます。
- データが利用可能になり次第、出力することが可能です。これにより、大量のデータをメモリに保存することなく、ストリーミングが可能になります。例えば、ドキュメントのデータ出力は、解析の進行に合わせてファイルごとに行われるべきです。
ホバーの例では、出力されるLSIFグラフデータは以下のようになります。
// a vertex representing the document
{ id: 1, type: "vertex", label: "document", uri: "file:///Users/username/sample.ts", languageId: "typescript" }
// a vertex representing the range for the identifier bar
{ id: 4, type: "vertex", label: "range", start: { line: 0, character: 9}, end: { line: 0, character: 12 } }
// an edge saying that the document with id 1 contains the range with id 4
{ id: 5, type: "edge", label: "contains", outV: 1, inV: 4}
// a vertex representing the actual hover result
{ id: 6, type: "vertex", label: "hoverResult",
result: {
contents: [
{ language: "typescript", value: "function bar(): void" }
]
}
}
// an edge linking the hover result to the range.
{ id: 7, type: "edge", label: "textDocument/hover", outV: 4, inV: 6 }
対応するグラフは以下のようになります。
LSPは、ドキュメントのみをパラメータとして取るリクエスト(位置ベースではない)もサポートしています。コード理解に役立つリクエストの例としては、すべてのドキュメントシンボルのリストや、すべての折りたたみ範囲の計算があります。これらのリクエストは、LSIFでは[request, document]
-> 結果の形式でモデル化されます。
別の例を見てみましょう。
function bar(): void {
console.log('Hello World!');
}
上記の関数bar
を含むドキュメントの折りたたみ範囲の結果は、このように出力されます。
// a vertex representing the document
{ id: 1, type: "vertex", label: "document", uri: "file:///Users/username/sample.ts", languageId: "typescript" }
// a vertex representing the folding result
{ id: 2, type: "vertex", label: "foldingRangeResult", result: [ { startLine: 0, startCharacter: 20, endLine: 2, endCharacter: 1 } ] }
// an edge connecting the folding result to the document.
{ id: 3, type: "edge", label: "textDocument/foldingRange", outV: 1, inV: 2 }
これらは、LSIFがサポートするLSPリクエストのほんの2つの例にすぎません。LSIF仕様の現在のバージョンでは、ドキュメントシンボル、ドキュメントリンク、定義へ移動、宣言へ移動、型定義へ移動、すべての参照の検索、実装へ移動もサポートされています。
皆様からのフィードバックが必要です!
LSIF仕様について、初期段階で順調に進展しており、コミュニティとの対話を開始し、私たちが取り組んでいることを皆さんに知っていただきたいと考えています。フィードバックについては、課題「Language Server Index Format」にご意見をお寄せください。
利用開始方法
LSIFの利用を開始するには、以下のリソースをご覧ください。
- LSIF仕様 - このドキュメントでは、出力されるデータをコンパクトに保つために行われたいくつかの追加最適化についても説明しています。
- TypeScript用LSIFインデックス - TypeScript用のLSIFを生成するツールです。READMEにツールの使用方法が記載されています。
- LSIF用Visual Studio Code拡張機能 - LSIF JSONダンプを使用して言語理解機能を提供するVS Code用拡張機能です。新しいLSIFジェネレーターを実装する場合、この拡張機能を使用して任意のソースコードで検証できます。