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はシンボルのセマンティクス(例えば、何がシンボルの定義を構成するのか、またはメソッドが別のメソッドをオーバーライドするかどうか)も定義していません。したがって、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では[リクエスト, ドキュメント] -> 結果の形式でモデル化されます。
別の例を見てみましょう。
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ジェネレーターを実装する場合、この拡張機能を使用して任意のソースコードで検証できます。