言語サーバーインデックス形式 (LSIF)
2019年2月19日 Dirk Bäumer 著
チェックアウトなしでリッチなコードナビゲーション
開発者として、多くの時間をコードの読み取りとレビューに費やしており、必ずしも新しいソースコードを作成しているわけではありません。例えば、GitHub のようなリポジトリ内の既存のコードベースを閲覧したり、同僚のプルリクエストをレビューしたりすることがあります。
通常、ブランチをチェックアウトするか、リポジトリをクローンしてソースコードをローカルマシンにダウンロードし、お好みの開発ツールを開いて、ようやくコードを読んでナビゲートできるようになります。最初にリポジトリをクローンせずにこれができたら、クールだと思いませんか? ソースコードをダウンロードせずに、ホバー情報、定義へ移動、すべての参照の検索などのインテリジェントなコード機能を利用できることを想像してみてください。ブログ記事「リッチなコードナビゲーション体験の最初の взгляд」では、プルリクエストのレビューにおけるこのシナリオについて説明しています。
言語サーバーインデックス形式 (LSIF, 「else if」のように発音) の目標は、ソースコードのローカルコピーを必要とせずに、開発ツールまたは Web UI でのリッチなコードナビゲーションをサポートすることです。この形式は、リッチなコード編集機能を開発ツールに統合する作業を簡素化する言語サーバープロトコル (LSP) と精神的に似ています。
既存の LSP 言語サーバーを単純に使用しないのはなぜでしょうか? LSP は、オートコンプリート、タイプ時のフォーマット、リッチなコードナビゲーションなどのリッチなコード作成機能を提供します。これらの機能を効率的に提供するために、言語サーバーはすべてのソースコードファイルがローカルディスク上で利用可能であることを要求します。 LSP 言語サーバーは、ファイルの一部または全部をメモリに読み込み、これらの機能を強化するために抽象構文木を計算する場合もあります。言語サーバーインデックス形式の目標は、これらの要件なしでリッチなコードナビゲーション機能をサポートするために LSP プロトコルを拡張することです。 LSIF は、言語サーバーまたはその他のプログラミングツールがコードワークスペースに関する知識を出力するための標準形式を定義します。この永続化された情報は、後で言語サーバーを実行せずに同じワークスペースに対する LSP リクエストに答えるために使用できます。
言語サーバーインデックス形式
LSIF は LSP を基盤として構築されており、LSP で定義されているのと同じデータ型を使用しています。大まかに言って、LSIF は言語サーバーリクエストから返されるデータをモデル化します。 LSP と同様に、LSIF はプログラムシンボル情報を含まず、LSIF はシンボルセマンティクス (例えば、シンボルの定義を構成するものや、メソッドが別のメソッドをオーバーライドするかどうか) を定義しません。したがって、LSIF はシンボルデータベースを定義しません。これは LSP のアプローチと一致しています。
既存の LSP データ型を LSIF のベースとして使用することには、LSIF がすでに LSP を理解しているツールやサーバーに簡単に統合できるという別の利点もあります。
例を見てみましょう。まず、以下のコンテンツを含む 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]
-> result の形式でモデル化されています。
別の例を見てみましょう。
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 ジェネレーターを実装する場合、この拡張機能を使用して任意のソースコードで検証できます。