– 代理会话日,2月19日

语法高亮指南

语法高亮决定了 Visual Studio Code 编辑器中显示的源代码的颜色和样式。它负责将 JavaScript 中的 iffor 等关键字与字符串、注释和变量名区分开来,并以不同的颜色显示。

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

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

词法分析 (Tokenization)

文本的词法分析是指将文本分解为片段,并为每个片段分配一个词法单元类型。

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 词法单元和作用域

词法单元是程序元素的 1 个或多个字符。示例词法单元包括运算符,如 +*,变量名,如 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 编写语法

随着语法的复杂性增加,理解和维护它可能会变得困难。如果你发现自己正在编写复杂的正则表达式或需要添加注释来解释语法的某些方面,请考虑使用 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 指定要将语法注入到的目标语言作用域列表。

对于这个例子,我们将创建一个简单的注入语法,它将 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"
}

scope inspector

范围检查器显示以下信息

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