語法高亮指南
語法高亮決定了在 Visual Studio Code 編輯器中顯示原始碼的顏色和樣式。它負責將 JavaScript 中的 if
或 for
等關鍵字與字串、註釋和變數名區分開來。
語法高亮有兩個組成部分
在深入瞭解細節之前,一個好的開始是使用作用域檢查器工具,探索原始檔中存在哪些標記,以及它們匹配哪些主題規則。要檢視語義標記和語法標記,請在 TypeScript 檔案上使用內建主題(例如,深色+)。
分詞
文字的分詞是將文字分解成片段,並用標記型別對每個片段進行分類。
VS Code 的分詞引擎由TextMate 語法提供支援。TextMate 語法是正則表示式的結構化集合,以 plist (XML) 或 JSON 檔案形式編寫。VS Code 擴充套件可以透過 grammars
貢獻點貢獻語法。
TextMate 分詞引擎與渲染器在同一個程序中執行,標記會隨著使用者輸入而更新。標記用於語法高亮,也用於將原始碼分類為註釋、字串、正則表示式區域。
從 1.43 版本開始,VS Code 還允許擴充套件透過語義標記提供程式提供分詞。語義提供程式通常由語言伺服器實現,它們對原始檔有更深入的理解,並可以在專案上下文中解析符號。例如,一個常量變數名可以在整個專案中以常量高亮顯示,而不僅僅是在其宣告的地方。
基於語義標記的高亮被認為是基於 TextMate 的語法高亮的補充。語義高亮位於語法高亮之上。由於語言伺服器載入和分析專案可能需要一些時間,因此語義標記高亮可能會在短暫停頓後出現。
本文重點介紹基於 TextMate 的分詞。語義分詞和主題設定在語義高亮指南中解釋。
TextMate 語法
VS Code 使用TextMate 語法作為語法分詞引擎。TextMate 語法是為 TextMate 編輯器發明的,由於開源社群建立和維護了大量的語言包,它們已被許多其他編輯器和 IDE 採用。
TextMate 語法依賴於Oniguruma 正則表示式,通常以 plist 或 JSON 編寫。您可以在此處找到 TextMate 語法的良好介紹,您可以檢視現有的 TextMate 語法以瞭解它們的工作原理。
TextMate 標記和作用域
標記是一個或多個屬於同一程式元素的字元。示例標記包括運算子,如 +
和 *
,變數名,如 myVar
,或字串,如 "my string"
。
每個標記都與一個定義標記上下文的作用域相關聯。作用域是一個由點分隔的識別符號列表,用於指定當前標記的上下文。例如,JavaScript 中的 +
運算子具有作用域 keyword.operator.arithmetic.js
。
主題將作用域對映到顏色和樣式以提供語法高亮。TextMate 提供了許多主題都針對的常見作用域列表。為了使您的語法得到儘可能廣泛的支援,請嘗試在現有作用域的基礎上構建,而不是定義新的作用域。
作用域是巢狀的,因此每個標記也與父作用域列表相關聯。下面的示例使用作用域檢查器顯示簡單 JavaScript 函式中 +
運算子的作用域層次結構。最具體的範圍列在頂部,更一般的父範圍列在下方
父作用域資訊也用於主題設定。當主題針對一個作用域時,所有具有該父作用域的標記都將被著色,除非主題還為它們的單個作用域提供了更具體的著色。
貢獻基本語法
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
語法將字母 a
、b
和 c
標記為關鍵字,並將括號巢狀標記為表示式。
{
"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
)將包含在當前作用域中。檔案末尾的最後一個括號是 expression.group
的一部分,即使 end
規則不匹配,因為在 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
選項
Yeoman 將引導您完成一些基本問題,以搭建新擴充套件的骨架。建立新語法的重要問題是
Language id
- 您的語言的唯一識別符號。Language name
- 您的語言的人類可讀名稱。Scope names
- 您的語法的根 TextMate 作用域名稱。
生成器假定您要為該語言定義新的語言和新的語法。如果您正在為現有語言建立語法,只需填寫目標語言的資訊,並務必刪除生成的 package.json
中的 languages
貢獻點。
回答所有問題後,Yeoman 將建立一個具有以下結構的新擴充套件
請記住,如果您正在為 VS Code 已知的語言貢獻語法,請務必刪除生成的 package.json
中的 languages
貢獻點。
轉換現有 TextMate 語法
yo code
還可以幫助將現有 TextMate 語法轉換為 VS Code 擴充套件。同樣,首先執行 yo code
並選擇 Language extension
。當被問及現有語法檔案時,提供 .tmLanguage
或 .json
TextMate 語法檔案的完整路徑
使用 YAML 編寫語法
隨著語法變得越來越複雜,將其作為 JSON 理解和維護可能會變得困難。如果您發現自己正在編寫複雜的正則表示式或需要添加註釋來解釋語法的各個方面,請考慮使用 YAML 來定義語法。
YAML 語法與基於 JSON 的語法結構完全相同,但允許您使用 YAML 更簡潔的語法,以及多行字串和註釋等功能。
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 語法。注入語法的示例應用
- 高亮註釋中的關鍵字,例如
TODO
。 - 向現有語法新增更具體的作用域資訊。
- 為 Markdown 圍欄程式碼塊新增新語言的高亮。
建立基本注入語法
注入語法與常規語法一樣透過 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"
}
作用域檢查器顯示以下資訊
- 當前標記。
- 有關標記的元資料及其計算外觀的資訊。如果您正在使用嵌入式語言,這裡重要的條目是
language
和token type
。 - 噹噹前語言存在語義標記提供程式且當前主題支援語義高亮時,將顯示語義標記部分。它顯示當前語義標記型別和修飾符,以及匹配語義標記型別和修飾符的主題規則。
- TextMate 部分顯示當前 TextMate 標記的作用域列表,最具體的範圍在頂部。它還顯示匹配作用域的最具體主題規則。這隻顯示負責標記當前樣式的主題規則,不顯示被覆蓋的規則。如果存在語義標記,則僅當主題規則與匹配語義標記的規則不同時才顯示它們。