語法突顯指南

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

語法突顯由兩個組件組成:

在深入細節之前,建議先使用範圍偵測器 (scope inspector) 工具,探索原始程式碼檔案中存在哪些語彙單元,以及它們符合哪些佈景主題規則。若要同時查看語意與語法語彙單元,請在 TypeScript 檔案上使用內建佈景主題(例如 Dark+)。

語彙分析

語彙分析是將文字分解為片段,並為每個片段分類語彙單元類型的過程。

VS Code 的語彙分析引擎採用 TextMate 文法驅動。TextMate 文法是結構化的正規表示式集合,通常以 plist (XML) 或 JSON 檔案編寫。VS Code 擴充功能可以透過 grammars 貢獻點來提供文法。

TextMate 語彙分析引擎與轉譯器 (renderer) 在同一個程序中執行,當使用者輸入時,語彙單元會即時更新。語彙單元不僅用於語法突顯,還用於將原始程式碼歸類為註解、字串、正規表示式等區域。

從 1.43 版本開始,VS Code 也允許擴充功能透過 語意語彙單元提供者 (Semantic Token Provider) 提供語彙分析。語意提供者通常由對原始檔案有更深入理解的語言伺服器實現,能夠解析專案上下文中的符號。例如,常數變數名稱可以在整個專案中以常數突顯方式呈現,而不僅僅是在宣告的位置。

基於語意語彙單元的突顯被視為對基於 TextMate 的語法突顯的補充。語意突顯會疊加在語法突顯之上。由於語言伺服器載入和分析專案可能需要時間,語意語彙單元突顯可能會在短暫延遲後出現。

本文主要介紹基於 TextMate 的語彙分析。語意語彙分析與佈景主題設定請參閱 語意突顯指南

TextMate 文法

VS Code 使用 TextMate 文法作為語法語彙分析引擎。它最初是為 TextMate 編輯器發明的,因開源社群維護了大量的語言套件,現已被許多其他編輯器和 IDE 採用。

TextMate 文法依賴 Oniguruma 正規表示式,通常以 plist 或 JSON 格式編寫。您可以從這裡找到 TextMate 文法的入門介紹,也可以查看現有的 TextMate 文法以深入了解其運作方式。

TextMate 語彙單元與範圍 (Scopes)

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

每個語彙單元都關聯一個定義其上下文的「範圍」(scope)。範圍是以點分隔的識別碼列表,用以指定目前語彙單元的上下文。例如,JavaScript 中的 + 運算,其範圍為 keyword.operator.arithmetic.js

佈景主題將範圍對應到顏色與樣式以提供語法突顯。TextMate 提供了一份通用範圍列表,許多佈景主題皆針對此設計。為了讓您的文法獲得最廣泛的支援,建議盡量基於現有的範圍來構建,而非定義新的範圍。

範圍具有巢狀結構,每個語彙單元也會關聯一組父範圍列表。下例使用範圍偵測器來顯示一個簡單 JavaScript 函式中 + 運算子的範圍階層。最明確的範圍列於最上方,較通用的父範圍則列於下方。

syntax highlighting scopes

父範圍資訊也用於佈景主題設定。當佈景主題針對某個範圍設定時,所有具有該父範圍的語彙單元都會被著色,除非該佈景主題為它們各自的範圍提供了更明確的顏色設定。

配置括號匹配範圍

某些語言包含視覺上類似括號,但不應參與括號匹配的語彙單元。

配置括號匹配行為有兩個屬性:

  • balancedBracketScopes:定義哪些範圍參與括號匹配。預設情況下,包含所有範圍。
  • unbalancedBracketScopes:定義應從括號匹配中排除的範圍。
{
  "unbalancedBracketScopes": ["meta.scope.case-pattern.shell"]
}

貢獻一個基本文法

VS Code 支援 JSON 格式的 TextMate 文法。這些文法透過 grammars 貢獻點進行貢獻。

每個文法貢獻皆需指定:文法適用的語言識別碼、文法語彙單元的頂層範圍名稱,以及指向文法檔案的相對路徑。下例展示了虛構 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

該範例文法會產生以下範圍(從最明確到最不明確,由左至右列出):

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)會包含在目前範圍中。即使未匹配到 end 規則,檔案末尾的最後一個括號仍屬於 expression.group,因為在 end 規則之前已發現 end-of-document

嵌入式語言

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

embeddedLanguages 貢獻點將嵌入語言中的範圍對應到頂層語言範圍。在下例中,meta.embedded.block.javascript 範圍內的任何語彙單元都將被視為 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 範圍名稱。

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

注入文法 (Injection grammars)

注入文法允許您擴充現有的文法。注入文法是一個普通的 TextMate 文法,會被「注入」到現有文法的特定範圍內。注入文法的應用範例:

  • 在註解中突顯 TODO 等關鍵字。
  • 為現有文法添加更具體的範圍資訊。
  • 為 Markdown 圍欄程式碼區塊 (fenced code blocks) 添加新語言的突顯。

建立基本注入文法

注入文法與普通文法一樣,透過 package.json 進行貢獻。但注入文法不指定 language,而是使用 injectTo 指定要注入該文法的目標語言範圍列表。

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

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

該文法本身是一個標準的 TextMate 文法,除了頂層的 injectionSelector 項目。injectionSelector 是一個範圍選取器,指定注入文法應套用到哪些範圍。在我們的範例中,我們想要突顯所有 // 註解中的 TODO。使用範圍偵測器,我們發現 JavaScript 的雙斜線註解範圍是 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 將嵌入語言的範圍對應到頂層語言範圍。

例如,一個在 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.* 範圍來重設 VS Code 對語彙單元的標記(視其為字串或註解內容)。建議始終將嵌入語言包裹在 meta.embedded.* 範圍內,以確保 VS Code 能正確處理嵌入語言。

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

{
  "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 範圍選取器以及對應的顏色與樣式。

評估語彙單元的顏色和樣式時,目前語彙單元的範圍會與規則的選取器進行匹配,以找到每個樣式屬性(前景、粗體、斜體、底線)最明確的規則。

顏色佈景主題指南說明了如何建立顏色佈景主題。語意語彙單元的佈景主題設定請參閱 語意突顯指南

範圍偵測器

VS Code 的內建範圍偵測器工具可協助偵錯文法與語意語彙單元。它會顯示檔案中目前位置的語彙單元範圍與語意語彙單元,以及關於哪些佈景主題規則套用到該語彙單元的元數據。

您可以從命令面板使用 Developer: Inspect Editor Tokens and Scopes 命令觸發範圍偵測器,或為其建立鍵盤快速鍵

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

scope inspector

範圍偵測器會顯示以下資訊:

  1. 目前的語彙單元。
  2. 關於該語彙單元的元數據及其計算出的外觀資訊。如果您正在使用嵌入語言,這裡的重要項目是 languagetoken type
  3. 當目前語言有可用的語意語彙單元提供者,且目前佈景主題支援語意突顯時,會顯示語意語彙單元區段。它會顯示目前的語意語彙單元類型與修飾詞,以及符合該類型與修飾詞的佈景主題規則。
  4. TextMate 區段會顯示目前 TextMate 語彙單元的範圍列表,最明確的範圍位於上方。它也會顯示符合該範圍的最明確佈景主題規則。此區段僅顯示對該語彙單元當前樣式負責的佈景主題規則,不會顯示被覆寫的規則。如果有語意語彙單元,則僅在佈景主題規則與匹配語意語彙單元的規則不同時,才會顯示這些規則。
© . This site is unofficial and not affiliated with Microsoft.