VS Codeのエージェントモードを拡張するには、を試してください!

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のベースとして使用することには、別の利点があります。LSIFは、LSPをすでに理解しているツールやサーバーに簡単に統合できるためです。

例を見てみましょう。以下の内容を持つsample.tsという単純なTypeScriptファイルから始めます。

function bar(): void {}

bar()にカーソルを合わせると、Visual Studio Codeに以下のホバー情報が表示されます。


Hover over Bar


このホバー情報は、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のすべての位置でホバー値は同じです。これは、ユーザーがbarbまたはbarrにカーソルを合わせても同じホバー値が返されることを意味します。出力されるデータをよりコンパクトにするために、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 }

対応するグラフは次のようになります。

LSIF graph for a hover

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 graph for a folding range result

これらは、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ジェネレータを実装する場合は、この拡張機能を使用して任意のソースコードで検証できます。