🚀 在 VS Code 中

語法突顯指南

語法突顯決定了 Visual Studio Code 編輯器中顯示的原始碼的色彩和樣式。它負責以不同於字串、註解和變數名稱的方式,為 JavaScript 中的關鍵字(如 iffor)著色。

語法突顯包含兩個組件

  • 符號化:將文字分解為符號列表
  • 佈景主題:使用佈景主題或使用者設定將符號對應到特定的色彩和樣式

在深入探討細節之前,一個好的開始是使用 Scope 檢查器 工具,並探索原始碼檔案中存在哪些符號,以及它們符合哪些佈景主題規則。若要同時查看語意和語法符號,請在 TypeScript 檔案上使用內建佈景主題(例如 Dark+)。

符號化

文字的符號化是將文字分解成段落,並將每個段落分類為符號類型。

VS Code 的符號化引擎由 TextMate 文法 驅動。TextMate 文法是規則運算式的結構化集合,並以 plist (XML) 或 JSON 檔案的形式撰寫。VS Code 擴充功能可以透過 grammars 貢獻點貢獻文法。

TextMate 符號化引擎與渲染器在同一個進程中執行,並且符號會隨著使用者輸入而更新。符號用於語法突顯,但也用於將原始碼分類為註解、字串、規則運算式等區域。

從 1.43 版本開始,VS Code 也允許擴充功能透過 語意符號提供者 提供符號化。語意提供者通常由語言伺服器實作,這些伺服器對原始碼檔案有更深入的了解,並且可以在專案的上下文中解析符號。例如,常數變數名稱可以在整個專案中使用常數突顯來呈現,而不僅僅是在其宣告的位置。

基於語意符號的突顯被視為 TextMate 基礎語法突顯的補充。語意突顯位於語法突顯之上。並且由於語言伺服器可能需要一段時間才能載入和分析專案,因此語意符號突顯可能會在短暫延遲後出現。

本文重點介紹基於 TextMate 的符號化。《語意突顯指南》中說明了語意符號化和佈景主題。

TextMate 文法

VS Code 使用 TextMate 文法 作為語法符號化引擎。它們為 TextMate 編輯器而發明,由於開放原始碼社群建立和維護了大量的語言套件,因此已被許多其他編輯器和 IDE 採用。

TextMate 文法依賴 Oniguruma 規則運算式,並且通常以 plist 或 JSON 撰寫。您可以在 這裡 找到 TextMate 文法的良好介紹,並且您可以查看現有的 TextMate 文法,以了解更多關於它們如何運作的資訊。

TextMate 符號和 Scope

符號是一個或多個屬於同一個程式元素的字元。範例符號包括運算子(例如 +*)、變數名稱(例如 myVar)或字串(例如 "my string")。

每個符號都與一個 Scope 相關聯,該 Scope 定義了符號的上下文。Scope 是以點分隔的識別碼列表,用於指定目前符號的上下文。例如,JavaScript 中的 + 運算具有 Scope keyword.operator.arithmetic.js

佈景主題將 Scope 對應到色彩和樣式,以提供語法突顯。TextMate 提供了 許多佈景主題目標的常見 Scope 列表。為了使您的文法獲得盡可能廣泛的支援,請嘗試建立在現有的 Scope 之上,而不是定義新的 Scope。

Scope 會巢狀結構,因此每個符號也與父 Scope 列表相關聯。下面的範例使用 Scope 檢查器 來顯示簡單 JavaScript 函數中 + 運算子的 Scope 階層。最特定的 Scope 列在頂部,更一般的父 Scope 列在下方

syntax highlighting scopes

父 Scope 資訊也用於佈景主題。當佈景主題以 Scope 為目標時,除非佈景主題也為其個別 Scope 提供更具體的色彩,否則所有具有該父 Scope 的符號都將被著色。

貢獻基本文法

VS Code 支援 json TextMate 文法。這些文法透過 grammars 貢獻點 貢獻。

每個文法貢獻都指定:文法適用的語言識別碼、文法符號的頂層 Scope 名稱,以及文法檔案的相對路徑。下面的範例顯示了虛構的 abc 語言的文法貢獻

{
  "contributes": {
    "languages": [
      {
        "id": "abc",
        "extensions": [".abc"]
      }
    ],
    "grammars": [
      {
        "language": "abc",
        "scopeName": "source.abc",
        "path": "./syntaxes/abc.tmGrammar.json"
      }
    ]
  }
}

文法檔案本身包含一個頂層規則。這通常分為一個 patterns 區段,其中列出了程式的頂層元素,以及一個 repository,其中定義了每個元素。文法中的其他規則可以使用 { "include": "#id" } 引用 repository 中的元素。

範例 abc 文法將字母 abc 標記為關鍵字,並將括號的巢狀結構標記為運算式。

{
  "scopeName": "source.abc",
  "patterns": [{ "include": "#expression" }],
  "repository": {
    "expression": {
      "patterns": [{ "include": "#letter" }, { "include": "#paren-expression" }]
    },
    "letter": {
      "match": "a|b|c",
      "name": "keyword.letter"
    },
    "paren-expression": {
      "begin": "\\(",
      "end": "\\)",
      "beginCaptures": {
        "0": { "name": "punctuation.paren.open" }
      },
      "endCaptures": {
        "0": { "name": "punctuation.paren.close" }
      },
      "name": "expression.group",
      "patterns": [{ "include": "#expression" }]
    }
  }
}

文法引擎將嘗試連續將 expression 規則應用於文件中的所有文字。對於一個簡單的程式,例如

a
(
    b
)
x
(
    (
        c
        xyz
    )
)
(
a

範例文法產生以下 Scope(從最特定到最不特定的 Scope 從左到右列出)

a               keyword.letter, source.abc
(               punctuation.paren.open, expression.group, source.abc
    b           keyword.letter, expression.group, source.abc
)               punctuation.paren.close, expression.group, source.abc
x               source.abc
(               punctuation.paren.open, expression.group, source.abc
    (           punctuation.paren.open, expression.group, expression.group, source.abc
        c       keyword.letter, expression.group, expression.group, source.abc
        xyz     expression.group, expression.group, source.abc
    )           punctuation.paren.close, expression.group, expression.group, source.abc
)               punctuation.paren.close, expression.group, source.abc
(               punctuation.paren.open, expression.group, source.abc
a               keyword.letter, expression.group, source.abc

請注意,未被其中一個規則匹配的文字(例如字串 xyz)包含在目前的 Scope 中。檔案結尾的最後一個括號是 expression.group 的一部分,即使 end 規則未匹配,因為在 end 規則之前找到了 end-of-document

嵌入式語言

如果您的文法包含父語言中的嵌入式語言,例如 HTML 中的 CSS 樣式區塊,您可以使用 embeddedLanguages 貢獻點來告知 VS Code 將嵌入式語言視為與父語言不同的語言。這確保了括號匹配、註解和其他基本語言功能在嵌入式語言中按預期工作。

embeddedLanguages 貢獻點將嵌入式語言中的 Scope 對應到頂層語言 Scope。在下面的範例中,meta.embedded.block.javascript Scope 中的任何符號都將被視為 JavaScript 內容

{
  "contributes": {
    "grammars": [
      {
        "path": "./syntaxes/abc.tmLanguage.json",
        "scopeName": "source.abc",
        "embeddedLanguages": {
          "meta.embedded.block.javascript": "javascript"
        }
      }
    ]
  }
}

現在,如果您嘗試在標記為 meta.embedded.block.javascript 的符號集中註解程式碼或觸發程式碼片段,它們將獲得正確的 // JavaScript 樣式註解和正確的 JavaScript 程式碼片段。

開發新的文法擴充功能

若要快速建立新的文法擴充功能,請使用 VS Code 的 Yeoman 範本 執行 yo code 並選取 New Language 選項

Selecting the 'new language' template in 'yo code'

Yeoman 將引導您完成一些基本問題,以搭建新的擴充功能。建立新文法的重要問題是

  • Language id - 您的語言的唯一識別碼。
  • Language name - 您的語言的人類可讀名稱。
  • Scope names - 您的文法的根 TextMate Scope 名稱。

Filling in the 'new language' questions

產生器會假設您要為該語言同時定義新的語言和新的文法。如果您要為現有語言建立文法,只需填寫目標語言的資訊,並務必刪除產生的 package.json 中的 languages 貢獻點。

回答完所有問題後,Yeoman 將建立一個具有以下結構的新擴充功能

A new language extension

請記住,如果您要為 VS Code 已經知道的語言貢獻文法,請務必刪除產生的 package.json 中的 languages 貢獻點。

轉換現有的 TextMate 文法

yo code 也可以協助將現有的 TextMate 文法轉換為 VS Code 擴充功能。再次,從執行 yo code 並選取 Language extension 開始。當被要求提供現有的文法檔案時,請提供 .tmLanguage.json TextMate 文法檔案的完整路徑

Converting an existing TextMate grammar

使用 YAML 撰寫文法

隨著文法變得越來越複雜,以 json 形式理解和維護它可能會變得困難。如果您發現自己正在撰寫複雜的規則運算式,或需要新增註解來解釋文法的各個方面,請考慮使用 yaml 來定義您的文法。

Yaml 文法具有與基於 json 的文法完全相同的結構,但允許您使用 yaml 更簡潔的語法,以及多行字串和註解等功能。

A yaml grammar using multiline strings and comments

VS Code 只能載入 json 文法,因此基於 yaml 的文法必須轉換為 json。js-yaml 套件 和命令列工具使這變得容易。

# Install js-yaml as a development only dependency in your extension
$ npm install js-yaml --save-dev

# Use the command-line tool to convert the yaml grammar to json
$ npx js-yaml syntaxes/abc.tmLanguage.yaml > syntaxes/abc.tmLanguage.json

注入文法

注入文法可讓您擴充現有的文法。注入文法是一種常規的 TextMate 文法,它被注入到現有文法中的特定 Scope 中。注入文法的範例應用

  • 在註解中突顯關鍵字,例如 TODO
  • 為現有文法新增更具體的 Scope 資訊。
  • 為 Markdown fences 程式碼區塊新增新語言的突顯。

建立基本注入文法

注入文法透過 package.json 貢獻,就像常規文法一樣。但是,注入文法不是指定 language,而是使用 injectTo 來指定要將文法注入到的目標語言 Scope 列表。

對於此範例,我們將建立一個簡單的注入文法,該文法將 JavaScript 註解中的 TODO 突顯為關鍵字。為了在 JavaScript 檔案中應用我們的注入文法,我們在 injectTo 中使用 source.js 目標語言 Scope

{
  "contributes": {
    "grammars": [
      {
        "path": "./syntaxes/injection.json",
        "scopeName": "todo-comment.injection",
        "injectTo": ["source.js"]
      }
    ]
  }
}

文法本身是一個標準的 TextMate 文法,除了頂層的 injectionSelector 條目。injectionSelector 是一個 Scope 選取器,它指定應在哪些 Scope 中應用注入的文法。對於我們的範例,我們希望在所有 // 註解中突顯單字 TODO。使用 Scope 檢查器,我們發現 JavaScript 的雙斜線註解具有 Scope comment.line.double-slash,因此我們的注入選取器是 L:comment.line.double-slash

{
  "scopeName": "todo-comment.injection",
  "injectionSelector": "L:comment.line.double-slash",
  "patterns": [
    {
      "include": "#todo-keyword"
    }
  ],
  "repository": {
    "todo-keyword": {
      "match": "TODO",
      "name": "keyword.todo"
    }
  }
}

注入選取器中的 L: 表示注入被新增到現有文法規則的左側。這基本上表示我們的注入文法的規則將在任何現有文法規則之前應用。

嵌入式語言

注入文法也可以為其父文法貢獻嵌入式語言。就像常規文法一樣,注入文法可以使用 embeddedLanguages 將嵌入式語言中的 Scope 對應到頂層語言 Scope。

例如,一個在 JavaScript 字串中突顯 SQL 查詢的擴充功能可以使用 embeddedLanguages 來確保標記為 meta.embedded.inline.sql 的字串內的所有符號都被視為 SQL,以用於括號匹配和程式碼片段選取等基本語言功能。

{
  "contributes": {
    "grammars": [
      {
        "path": "./syntaxes/injection.json",
        "scopeName": "sql-string.injection",
        "injectTo": ["source.js"],
        "embeddedLanguages": {
          "meta.embedded.inline.sql": "sql"
        }
      }
    ]
  }
}

符號類型和嵌入式語言

對於注入語言嵌入式語言,還有一個額外的複雜性:預設情況下,VS Code 將字串中的所有符號視為字串內容,並將註解中的所有符號視為符號內容。由於括號匹配和自動關閉配對等功能在字串和註解內被停用,因此如果嵌入式語言出現在字串或註解內,這些功能也將在嵌入式語言中被停用。

若要覆寫此行為,您可以使用 meta.embedded.* Scope 來重設 VS Code 將符號標記為字串或註解內容的行為。始終將嵌入式語言包裝在 meta.embedded.* Scope 中是一個好主意,以確保 VS Code 正確處理嵌入式語言。

如果您無法將 meta.embedded.* Scope 新增到您的文法中,您可以選擇在文法的貢獻點中使用 tokenTypes 將特定的 Scope 對應到內容模式。下面的 tokenTypes 區段確保 my.sql.template.string Scope 中的任何內容都被視為原始碼

{
  "contributes": {
    "grammars": [
      {
        "path": "./syntaxes/injection.json",
        "scopeName": "sql-string.injection",
        "injectTo": ["source.js"],
        "embeddedLanguages": {
          "my.sql.template.string": "sql"
        },
        "tokenTypes": {
          "my.sql.template.string": "other"
        }
      }
    ]
  }
}

佈景主題

佈景主題是關於為符號分配色彩和樣式。佈景主題規則在色彩佈景主題中指定,但使用者可以在使用者設定中自訂佈景主題規則。

TextMate 佈景主題規則在 tokenColors 中定義,並且具有與常規 TextMate 佈景主題相同的語法。每個規則都定義了一個 TextMate Scope 選取器以及產生的色彩和樣式。

在評估符號的色彩和樣式時,會將目前符號的 Scope 與規則的選取器進行匹配,以找到每個樣式屬性(前景、粗體、斜體、底線)的最特定規則

色彩佈景主題指南 說明瞭如何建立色彩佈景主題。《語意突顯指南》中說明了語意符號的佈景主題。

Scope 檢查器

VS Code 的內建 Scope 檢查器工具可協助偵錯文法和語意符號。它顯示檔案中目前位置的符號和語意符號的 Scope,以及有關哪些佈景主題規則適用於該符號的中繼資料。

從命令面板使用 Developer: Inspect Editor Tokens and Scopes 命令觸發 Scope 檢查器,或為其 建立鍵盤快速鍵

{
  "key": "cmd+alt+shift+i",
  "command": "editor.action.inspectTMScopes"
}

scope inspector

Scope 檢查器顯示以下資訊

  1. 目前的符號。
  2. 關於符號的中繼資料以及有關其計算外觀的資訊。如果您正在使用嵌入式語言,則此處的重要條目是 languagetoken type
  3. 當目前語言有語意符號提供者可用,並且目前佈景主題支援語意突顯時,會顯示語意符號區段。它顯示目前的語意符號類型和修飾詞,以及符合語意符號類型和修飾詞的佈景主題規則。
  4. TextMate 區段顯示目前 TextMate 符號的 Scope 列表,最特定的 Scope 位於頂部。它還顯示了符合 Scope 的最特定佈景主題規則。這僅顯示負責符號目前樣式的佈景主題規則,它不顯示被覆寫的規則。如果存在語意符號,則僅當佈景主題規則與符合語意符號的規則不同時才顯示佈景主題規則。