尝试以扩展 VS Code 中的代理模式!

语法高亮指南

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

语法高亮有两个组成部分

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

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

分词

文本的分词是将文本分解为片段,并用令牌类型对每个片段进行分类。

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 函数中 + 运算符的作用域层次结构。最具体的作用域列在顶部,更通用的父作用域列在下面

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 语法将字母 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"]
      }
    ]
  }
}

语法本身是标准的 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 令牌的作用域列表,最具体的作用域在顶部。它还显示与作用域匹配的最具体的主题规则。这仅显示负责令牌当前样式的主题规则,它不显示被覆盖的规则。如果存在语义令牌,则仅当它们与匹配语义令牌的规则不同时才显示主题规则。