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

嵌入式程式語言

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 庫對您來說是個問題,您可以考慮請求轉發方法。