参加你附近的 ,了解 VS Code 中的 AI 辅助开发。

语法高亮指南

语法高亮决定了在 Visual Studio Code 编辑器中显示的源代码的颜色和样式。它负责将 JavaScript 中的 `if` 或 `for` 等关键字与字符串、注释和变量名进行不同的着色。

语法高亮有两个组成部分:

  • 分词:将文本分解为词元列表。
  • 主题化:使用主题或用户设置将词元映射到特定的颜色和样式。

在深入了解细节之前,一个好的开始是使用范围检查器工具,探索源文件中存在哪些词元以及它们匹配哪些主题规则。要查看语义词元和语法词元,请在 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 词元和范围

词元是一个或多个属于同一程序元素的字符。示例词元包括运算符,例如 `+` 和 `*`,变量名,例如 `myVar`,或字符串,例如 `"my string"`。

每个词元都与一个定义词元上下文的范围相关联。范围是点分隔的标识符列表,用于指定当前词元的上下文。例如,JavaScript 中的 `+` 操作符的范围是 `keyword.operator.arithmetic.js`。

主题将范围映射到颜色和样式以提供语法高亮。TextMate 提供了许多主题针对的常见范围列表。为了让您的语法得到尽可能广泛的支持,请尽量基于现有范围构建,而不是定义新范围。

范围是嵌套的,因此每个词元也与父范围列表相关联。下面的示例使用范围检查器显示简单 JavaScript 函数中 `+` 运算符的范围层次结构。最具体的范围列在顶部,更通用的父范围列在下面。

syntax highlighting scopes

父范围信息也用于主题化。当主题针对某个范围时,所有具有该父范围的词元都将被着色,除非主题还为其单独的范围提供了更具体的着色。

贡献一个基本语法

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` 选项。

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

注入语法

注入语法允许您扩展现有语法。注入语法是注入到现有语法中特定范围内的常规 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"
}

scope inspector

范围检查器显示以下信息:

  1. 当前词元。
  2. 有关词元的元数据及其计算出的外观信息。如果您正在使用嵌入式语言,此处的重要条目是 `language` 和 `token type`。
  3. 当当前语言有语义词元提供程序可用且当前主题支持语义高亮时,将显示语义词元部分。它显示当前语义词元类型和修饰符,以及与语义词元类型和修饰符匹配的主题规则。
  4. TextMate 部分显示当前 TextMate 词元的范围列表,最具体的范围在顶部。它还显示与范围匹配的最具体的主题规则。这只显示负责词元当前样式的主题规则,它不显示被覆盖的规则。如果存在语义词元,则仅当主题规则与匹配语义词元的规则不同时才显示主题规则。
© . This site is unofficial and not affiliated with Microsoft.