语法高亮指南

语法高亮决定了在 Visual Studio Code 编辑器中显示的源代码的颜色和样式。它负责为 JavaScript 中的 iffor 等关键字上色,使其与字符串、注释和变量名有所区别。

语法高亮包含两个组成部分

在深入了解细节之前,一个好的开始是使用作用域检查器(scope inspector)工具,探索源文件中存在哪些词法单元以及它们匹配了哪些主题规则。要同时查看语义词法单元和语法词法单元,请在 TypeScript 文件上使用内置主题(例如 Dark+)。

词法分析

文本的词法分析是指将文本划分为多个片段,并为每个片段分类一个词法单元类型(token type)。

VS Code 的词法分析引擎由 TextMate 语法驱动。TextMate 语法是正则表达式的结构化集合,通常以 plist (XML) 或 JSON 文件编写。VS Code 扩展可以通过 grammars 贡献点来贡献语法。

TextMate 词法分析引擎与渲染器运行在同一个进程中,并且词法单元会随着用户的键入而更新。词法单元不仅用于语法高亮,还用于将源代码分类为注释、字符串、正则表达式等区域。

从 1.43 版本开始,VS Code 还允许扩展通过语义词法单元提供程序(Semantic Token Provider)提供词法分析。语义提供程序通常由对源文件有更深入了解并能在项目上下文中解析符号的语言服务器来实现。例如,常量变量名可以在整个项目中都使用常量高亮显示进行渲染,而不仅仅在其声明的地方。

基于语义词法单元的高亮显示被认为是基于 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

父作用域信息也用于主题设置。当主题以某个作用域为目标时,具有该父作用域的所有词法单元都将被着色,除非该主题还为其各自的作用域提供了更具体的着色。

配置括号匹配作用域

某些语言包含不应参与括号匹配的词法单元,尽管它们在视觉上类似于括号。

有两个用于配置括号匹配行为的属性

  • 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)会被包含在当前作用域中。文件末尾的最后一个括号是 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"]
      }
    ]
  }
}

除了顶级的 injectionSelector 条目外,该语法本身是一个标准的 TextMate 语法。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 词法单元的作用域列表,最具体的作用域在顶部。它还显示匹配作用域的最具体主题规则。这仅显示负责词法单元当前样式的主题规则,不显示被覆盖的规则。如果存在语义词法单元,则仅当主题规则与匹配语义词法单元的规则不同时才显示它们。
English 한국어 中文(简体) 中文(繁體)
© . This website operates independently and is not affiliated with or endorsed by Microsoft. All brand names, logos, and trademarks are the property of their respective owners.