语法高亮指南

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

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

  • Tokenization: 将文本分解为一系列标记
  • Theming: 使用主题或用户设置将令牌映射到特定的颜色和样式

在深入细节之前,一个好的开始是使用scope inspector工具,探索源文件中存在哪些标记以及它们匹配的主题规则。要查看语义和语法标记,请在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函数中+运算符的作用域层次结构。最具体的作用域列在最上面,更一般的父作用域列在下面:

语法高亮范围

父作用域信息也用于主题设置。当主题针对一个作用域时,所有具有该父作用域的标记将被着色,除非主题还为它们的个别作用域提供了更具体的着色。

贡献一个基础语法

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选项:

在'yo code'中选择'新语言'模板

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 语法文件的完整路径:

转换现有的TextMate语法

使用YAML编写语法

随着语法的复杂性增加,理解和维护其作为json格式可能会变得困难。如果你发现自己正在编写复杂的正则表达式或需要添加注释来解释语法的某些方面,考虑使用yaml来定义你的语法。

Yaml语法与基于json的语法具有完全相同的结构,但允许您使用yaml更简洁的语法,以及多行字符串和注释等功能。

使用多行字符串和注释的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来指定要注入语法的目标语言范围列表。

对于这个例子,我们将创建一个简单的注入语法,将TODO高亮显示为JavaScript注释中的关键字。为了在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"
}

范围检查器

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

  1. 当前令牌。
  2. 关于令牌的元数据及其计算外观的信息。如果您正在处理嵌入式语言,这里的重要条目是languagetoken type
  3. 当当前语言有语义标记提供者且当前主题支持语义高亮时,会显示语义标记部分。它显示当前的语义标记类型和修饰符,以及匹配语义标记类型和修饰符的主题规则。
  4. TextMate 部分显示了当前 TextMate 令牌的作用域列表,最具体的作用域位于顶部。它还显示了与作用域匹配的最具体的主题规则。这里只显示负责令牌当前样式的主题规则,不会显示被覆盖的规则。如果存在语义令牌,则仅当主题规则与匹配语义令牌的规则不同时,才会显示主题规则。