🚀 在 VS Code 中免費取得

嵌入式程式語言

Visual Studio Code 為程式語言提供豐富的語言功能。如您在語言伺服器擴充功能指南中所讀到的,您可以編寫語言伺服器來支援任何程式語言。然而,為嵌入式語言啟用此類支援需要更多努力。

如今,嵌入式語言的數量不斷增加,例如

  • HTML 中的 JavaScript 和 CSS
  • JavaScript 中的 JSX
  • 範本語言中的內插,例如 Vue、Handlebars 和 Razor
  • PHP 中的 HTML

本指南著重於為嵌入式語言實作語言功能。如果您對為嵌入式語言提供語法突顯感興趣,您可以在語法突顯指南中找到相關資訊。

本指南包含兩個範例,說明建構此類語言伺服器的兩種方法:語言服務請求轉發。我們將檢閱這兩個範例,並總結每種方法的優缺點。

這兩個範例的原始碼都可以在以下位置找到

以下是我們將建構的嵌入式語言伺服器

sample

這兩個範例都為了說明目的而貢獻了一種新的語言 html1。您可以建立一個 .html1 檔案並測試以下功能

  • HTML 標籤的完成
  • <style> 標籤中 CSS 的完成
  • CSS 的診斷 (僅在語言服務範例中)

語言服務

語言服務是一個程式庫,它為單一語言實作程式化語言功能語言伺服器可以嵌入語言服務來處理嵌入式語言。

以下是 VS Code HTML 支援的概要

HTML 語言伺服器分析 HTML 文件,將其分解為語言區域,並使用對應的語言服務來處理語言伺服器請求。

例如

  • 對於 <| 位置的自動完成請求,HTML 語言伺服器使用 HTML 語言服務來提供 HTML 完成。
  • 對於 <style>.foo { | }</style> 位置的自動完成請求,HTML 語言伺服器使用 CSS 語言服務來提供 CSS 完成。

讓我們檢視 lsp-embedded-language-service 範例,這是一個簡化的 HTML 語言伺服器版本,它實作了 HTML 和 CSS 的自動完成,以及 CSS 的診斷錯誤。

語言服務範例

注意:此範例假設您已具備程式化語言功能主題語言伺服器擴充功能指南的知識。此程式碼建立在 lsp-sample 之上。

原始碼可在 microsoft/vscode-extension-samples 取得。

lsp-sample 相比,用戶端程式碼是相同的。

如上所述,伺服器將文件分解為不同的語言區域以處理嵌入式內容。

這是一個簡單的範例

<div></div>
<style>.foo { }</style>

在這種情況下,伺服器偵測到 <style> 標籤,並將 .foo { } 標記為 CSS 區域。

給定特定位置的自動完成請求,伺服器使用以下邏輯來計算回應

  • 如果位置落在任何區域內
    • 使用具有該區域語言的虛擬文件來處理它,同時將所有其他區域替換為空白字元
  • 如果位置落在任何區域之外
    • 使用 HTML 中的虛擬文件來處理它,同時將所有區域替換為空白字元

例如,當在這個位置進行自動完成時

<div></div>
<style>.foo { | }</style>

伺服器確定該位置在區域內,並計算具有以下內容的虛擬 CSS 文件 (█ 代表空格)

███████████
███████.foo { | }████████

然後,伺服器使用 vscode-css-languageservice 來分析此文件並計算完成項目列表。由於內容現在不包含任何 HTML,因此 CSS 語言服務可以順利處理它。透過將所有非 CSS 內容替換為空白字元,我們省去了手動偏移位置的麻煩。

伺服器程式碼處理完成請求

connection.onCompletion(async (textDocumentPosition, token) => {
  const document = documents.get(textDocumentPosition.textDocument.uri);
  if (!document) {
    return null;
  }

  const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
  if (!mode || !mode.doComplete) {
    return CompletionList.create();
  }
  const doComplete = mode.doComplete!;

  return doComplete(document, textDocumentPosition.position);
});

CSS 模式負責處理所有落在 CSS 區域內的語言伺服器請求

export function getCSSMode(
  cssLanguageService: CSSLanguageService,
  documentRegions: LanguageModelCache<HTMLDocumentRegions>
): LanguageMode {
  return {
    getId() {
      return 'css';
    },
    doComplete(document: TextDocument, position: Position) {
      // Get virtual CSS document, with all non-CSS code replaced with whitespace
      const embedded = documentRegions.get(document).getEmbeddedDocument('css');
      // Compute a response with vscode-css-languageservice
      const stylesheet = cssLanguageService.parseStylesheet(embedded);
      return cssLanguageService.doComplete(embedded, position, stylesheet);
    }
  };
}

這是一種處理嵌入式語言的簡單而有效的方法。但是,這種方法有一些缺點

  • 您必須持續更新語言伺服器所依賴的語言服務。
  • 包含未使用與您的語言伺服器相同語言編寫的語言服務可能具有挑戰性。例如,以 PHP 編寫的 PHP 語言伺服器會發現包含以 TypeScript 編寫的 vscode-css-languageservice 很麻煩。

我們現在將介紹請求轉發,它可以解決上述問題。

請求轉發

簡而言之,請求轉發的工作方式與語言服務類似。請求轉發方法也接收語言伺服器請求,計算虛擬內容,並計算回應。

主要差異在於

  • 語言服務方法使用程式庫來計算語言伺服器回應,而請求轉發則將請求發送回 VS Code,以使用已啟動且已為嵌入式語言註冊完成提供者的擴充功能。

再次回到簡單的範例

<div></div>
<style>.foo { | }</style>

自動完成以這種方式發生

  • 語言用戶端使用 workspace.registerTextDocumentContentProviderembedded-content 文件註冊虛擬文字文件提供者。
  • 語言用戶端劫持 <FILE_URI> 的完成請求。
  • 語言用戶端確定請求位置落在 CSS 區域內。
  • 語言用戶端建構一個新的 URI,例如 embedded-content://css/<FILE_URI>.css
  • 然後,語言用戶端呼叫 commands.executeCommand('vscode.executeCompletionItemProvider', ...)
    • VS Code 的 CSS 語言伺服器回應此提供者請求。
    • 虛擬文字文件提供者為 CSS 語言伺服器提供虛擬內容,其中所有非 CSS 程式碼都替換為空白字元。
    • 語言用戶端從 VS Code 接收回應並將其作為回應發送。

透過這種方法,即使我們的程式碼不包含任何理解 CSS 的程式庫,我們也能夠計算 CSS 自動完成。隨著 VS Code 更新其 CSS 語言伺服器,我們無需更新程式碼即可獲得最新的 CSS 語言支援。

現在讓我們檢視範例程式碼。

請求轉發範例

注意:此範例假設您已具備程式化語言功能主題語言伺服器擴充功能指南的知識。此程式碼建立在 lsp-sample 之上。

原始碼可在 microsoft/vscode-extension-samples 取得。

保留文件 URI 與其虛擬文件之間的映射,並為對應的請求提供它們

const virtualDocumentContents = new Map<string, string>();

workspace.registerTextDocumentContentProvider('embedded-content', {
  provideTextDocumentContent: uri => {
    // Remove leading `/` and ending `.css` to get original URI
    const originalUri = uri.path.slice(1).slice(0, -4);
    const decodedUri = decodeURIComponent(originalUri);
    return virtualDocumentContents.get(decodedUri);
  }
});

透過使用語言用戶端的 middleware 選項,我們劫持了自動完成的請求

let clientOptions: LanguageClientOptions = {
  documentSelector: [{ scheme: 'file', language: 'html' }],
  middleware: {
    provideCompletionItem: async (document, position, context, token, next) => {
      // If not in `<style>`, do not perform request forwarding
      if (
        !isInsideStyleRegion(
          htmlLanguageService,
          document.getText(),
          document.offsetAt(position)
        )
      ) {
        return await next(document, position, context, token);
      }

      const originalUri = document.uri.toString(true);
      virtualDocumentContents.set(
        originalUri,
        getCSSVirtualContent(htmlLanguageService, document.getText())
      );

      const vdocUriString = `embedded-content://css/${encodeURIComponent(originalUri)}.css`;
      const vdocUri = Uri.parse(vdocUriString);
      return await commands.executeCommand<CompletionList>(
        'vscode.executeCompletionItemProvider',
        vdocUri,
        position,
        context.triggerCharacter
      );
    }
  }
};

潛在問題

在實作嵌入式語言伺服器時,我們遇到了許多問題。雖然我們還沒有完美的解決方案,但我們想在您也可能遇到這些問題時提前告知您。

難以實作語言功能

一般而言,跨語言區域邊界工作的語言功能更難以實作。例如,自動完成或懸停內容很容易實作,因為您可以偵測嵌入式內容的語言並根據嵌入式內容計算回應。但是,格式化或重新命名等語言功能可能需要特殊處理。在格式化的情況下,您需要處理單一文件中多個區域的縮排和格式化工具設定。對於重新命名,使其跨不同文件中的不同區域工作可能具有挑戰性。

語言服務可能是有狀態且難以嵌入的

VS Code 的 HTML 支援提供 HTML、CSS 和 JavaScript 語言功能。雖然 HTML 和 CSS 語言服務是無狀態的,但為 JavaScript 語言功能提供支援的 TypeScript 伺服器是有狀態的。我們僅在 HTML 文件中提供基本的 JavaScript 支援,因為很難將專案狀態告知 TypeScript。例如,如果您包含一個指向 CDN 上託管的 lodash 程式庫的 <script> 標籤,您將不會在 <script> 標籤內獲得 _. 完成。

編碼和解碼

文件的主要語言可能具有與其嵌入式語言不同的編碼或逸出規則。例如,根據 HTML 規範,此 HTML 文件是無效的

<SCRIPT type="text/javascript">
  document.write ("<EM>This won't work</EM>")
</SCRIPT>

在這種情況下,如果嵌入式 JavaScript 的語言伺服器傳回包含 </ 的結果,則應將其逸出為 <\/

結論

這兩種方法各有優缺點。

語言服務

  • + 完全控制語言伺服器和使用者體驗。
  • + 不依賴其他語言伺服器。所有程式碼都在一個儲存庫中。
  • + 語言伺服器可以在所有符合 LSP 標準的程式碼編輯器中重複使用。
  • - 可能難以嵌入以其他語言編寫的語言服務。
  • - 需要持續維護才能從語言服務依賴項獲得新功能。

請求轉發

  • + 避免嵌入非以語言伺服器語言編寫的語言服務的問題 (例如,在 Razor 語言伺服器中嵌入 C# 編譯器以支援 C#)。
  • + 無需維護即可從其他語言服務獲得新的上游功能。
  • - 不適用於診斷錯誤。VS Code API 不支援可以「提取」(請求) 診斷的診斷提供者。
  • - 由於缺乏控制,很難與其他語言伺服器共用狀態。
  • - 跨語言功能可能難以實作 (例如,當存在 <div class="foo"> 時,為 .foo 提供 CSS 完成)。

總體而言,我們建議透過嵌入語言服務來建構語言伺服器,因為這種方法讓您可以更好地控制使用者體驗,並且伺服器可以重複用於任何符合 LSP 標準的編輯器。但是,如果您的使用案例很簡單,嵌入式內容可以在沒有上下文或語言伺服器狀態的情況下輕鬆處理,或者如果捆綁 Node.js 程式庫對您來說是個問題,您可以考慮請求轉發方法。