參加你附近的 ,瞭解 VS Code 中的 AI 輔助開發。

語言伺服器索引格式 (LSIF)

2019 年 2 月 19 日,作者:Dirk Bäumer

無需檢出程式碼即可實現豐富的程式碼導航

作為一名開發人員,您會花費大量時間來閱讀和審查程式碼,而不一定是在編寫新的原始碼。例如,您可能想要在 GitHub 這樣的程式碼倉庫中瀏覽現有的程式碼庫,或者想要審查同事的拉取請求 (Pull Request)。

通常,您需要檢出一個分支或克隆一個倉庫,將原始碼拉取到本地計算機上,然後開啟您喜歡的開發工具,最後才能閱讀和導航程式碼。如果無需先克隆倉庫就能做到這一點,那豈不是很酷?想象一下,無需下載原始碼就能獲得懸停資訊、轉到定義和查詢所有引用等智慧程式碼功能。博文《豐富的程式碼導航體驗初探》就闡述了在拉取請求審查中的這種場景。

語言伺服器索引格式 (LSIF,發音類似“else if”) 的目標是在開發工具或 Web UI 中支援豐富的程式碼導航,而無需本地原始碼副本。該格式在精神上類似於語言伺服器協議 (LSP),LSP 簡化了將豐富的程式碼編輯功能整合到開發工具中的過程。

為什麼不直接使用現有的 LSP 語言伺服器呢?LSP 提供了豐富的程式碼編寫功能,如自動完成、鍵入時格式化和豐富的程式碼導航。為了高效地提供這些功能,語言伺服器要求所有原始碼檔案都必須在本地磁碟上可用。LSP 語言伺服器還可能將部分或全部檔案讀入記憶體並計算抽象語法樹來支援這些功能。而語言伺服器索引格式的目標是擴充套件 LSP 協議,以便在沒有這些要求的情況下支援豐富的程式碼導航功能。LSIF 為語言伺服器或其他程式設計工具定義了一種標準格式,用於輸出它們關於程式碼工作區的知識。這些持久化的資訊隨後可以用來響應針對同一工作區的 LSP 請求,而無需執行語言伺服器。

語言伺服器索引格式

LSIF 建立在 LSP 之上,並使用與 LSP 中定義相同的資料型別。從高層次來看,LSIF 對語言伺服器請求返回的資料進行建模。與 LSP 相同,LSIF 不包含任何程式符號資訊,也不定義任何符號語義(例如,什麼構成了符號的定義,或者一個方法是否覆蓋了另一個方法)。因此,LSIF 並不定義符號資料庫,這與 LSP 的方法是一致的。

使用現有的 LSP 資料型別作為 LSIF 的基礎還有另一個優勢,因為 LSIF 可以輕鬆地整合到已經理解 LSP 的工具或伺服器中。

我們來看一個例子。我們從一個名為 sample.ts 的簡單 Typescript 檔案開始,其內容如下:

function bar(): void {}

在 Visual Studio Code 中,將滑鼠懸停在 bar() 上會顯示以下懸停資訊:


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 的所有位置,懸停值都是相同的。這意味著當用戶將滑鼠懸停在 bar 中的 br 上時,返回的懸停值是相同的。為了使輸出的資料更緊湊,LSIF 使用範圍 (range) 而不是位置 (position)。對於此示例,LSIF 工具會輸出元組 ['textDocument/hover', 'file:///Users/username/sample.ts', { start: { line: 0, character: 9 }, end: { line: 0, character: 12 }],其中包含了範圍資訊。

LSIF 使用圖 (graph) 來輸出這些資訊。在圖中,一個 LSP 請求由一條邊 (edge) 表示。文件、範圍或請求結果(例如懸停資訊)則由頂點 (vertex) 表示。這種格式有以下好處:

  • 對於給定的程式碼範圍,可以有不同的結果。對於給定的識別符號範圍,使用者可能對懸停值、定義位置或查詢所有引用感興趣。因此,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 中以 [請求, 文件] -> 結果 的形式建模。

我們再看另一個例子:

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 請求示例。當前版本的 LSIF 規範 還支援文件符號、文件連結、轉到定義、轉到宣告、轉到型別定義、查詢所有引用以及轉到實現。

我們需要您的反饋!

我們已經在 LSIF 規範上取得了良好的初步進展,我們希望向社群開放對話,以便大家可以瞭解我們正在進行的工作。如有反饋,請在問題 Language Server Index Format 中發表評論。

如何開始

要開始使用 LSIF,您可以檢視以下資源:

  • LSIF 規範 - 該文件還描述了一些為保持輸出資料緊湊而做的額外最佳化。
  • TypeScript 的 LSIF 索引器 - 一個為 TypeScript 生成 LSIF 的工具。README 檔案提供了該工具的使用說明。
  • 用於 LSIF 的 Visual Studio Code 擴充套件 - 一個 VS Code 擴充套件,它使用 LSIF JSON 轉儲檔案來提供語言理解功能。如果您實現一個新的 LSIF 生成器,可以使用此擴充套件來驗證其在任意原始碼上的效果。