1个不稳定版本
0.4.0 | 2024年5月17日 |
---|
#2006 在 开发工具
157 每月下载量
在 3 个包中使用(通过 nickel-lang-core)
285KB
4K SLoC
Topiary
Topiary旨在成为简单语言的统一格式化工具,作为Tree-sitter生态系统的一部分。它的名字来源于将树木修剪成奇异形状的艺术。
Topiary是为格式化工具作者和用户设计的。作者可以创建一种语言的格式化工具,而无需编写自己的格式化引擎或解析器。用户将受益于统一的代码风格,并在多个语言代码库中使用单一格式化工具,这些语言库应用了类似的风格。
动机
编写代码的样式在历史上主要是由个人选择决定的。当然,这从定义上是主观的,导致在审查格式化选择上浪费了大量时间,而不是代码本身。规定样式指南是早期的解决方案之一,产生了检查开发者格式化的工具,并最终导致了自动格式化工具的出现。后者由gofmt
推广,其开发者有洞察力,认为“足够好”的统一格式化,强制应用于代码库,在很大程度上解决了这些问题。
Topiary追求成为“通用格式化引擎”,不仅允许开发者自动使用统一风格格式化代码库,还可以使用简单的DSL定义新语言的风格。这允许快速开发格式化工具,前提是已为该语言定义了Tree-sitter语法。
设计原则
Topiary的创建目标是以下这些
-
使用Tree-sitter进行解析,以避免为格式化器编写另一套语法。
-
期望具有幂等性。也就是说,格式化已格式化的代码不会产生任何变化。
-
对于捆绑的格式化风格,应满足以下约束
-
与该语言在野外使用的已证实的格式化风格兼容。
-
忠实于作者的意图:如果代码被编写成跨越多行,该决定被保留。
-
最小化提交之间的变化,以便差异主要关注已更改的代码,而不是表面上的成果。也就是说,一行代码的变化不会影响其他代码,而格式化不会强迫你在修改代码时进行后续的表面性更改。
-
经过良好测试和健壮,以便格式化器可以在大型项目中得到信任。
-
-
对于最终用户(即不是格式化风格作者)而言,格式化器应该
-
规定一种格式化风格,虽然可以自定义,但对于代码库来说是统一和“足够好”的。
-
运行效率高。
-
提供与其他开发者工具(如编辑器和语言服务器)简单集成的便利。
-
语言支持
目前,Topiary针对的语言的Tree-sitter语法是静态链接的。这些语言的格式化风格分为两个成熟度级别:受支持和实验性。
受支持
这些格式化风格覆盖了目标语言,并实现了Topiary的既定设计目标。它们在Topiary中通过命令行标志暴露出来。
实验性
这些语言的格式化风格可能会发生变化,或者尚未考虑为生产就绪。它们可以通过指定查询文件路径在Topiary中访问。
入门指南
安装
可以从存储库目录使用Cargo构建和安装该项目。
cargo install --path topiary-cli
Topiary需要找到语言查询文件(.scm
)才能正常运行。默认情况下,topiary
在当前工作目录中寻找名为languages
的目录。
如果你不是从该存储库的其他目录运行Topiary,这将不起作用。为了无限制地使用Topiary,你必须设置环境变量TOPIARY_LANGUAGE_DIR
,使其指向Topiary的语言查询文件(.scm
)所在的目录。默认情况下,你应该将其设置为<topiary存储库的本地路径>/topiary-queries/queries
,例如
export TOPIARY_LANGUAGE_DIR=/home/me/tools/topiary/topiary-queries/queries
topiary fmt ./projects/helloworld/hello.ml
TOPIARY_LANGUAGE_DIR
可以在构建时设置。Topiary会自动选择相应的路径并将其嵌入到topiary
二进制文件中。在这种情况下,您再也不必担心在运行时使TOPIARY_LANGUAGE_DIR
可用。当TOPIARY_LANGUAGE_DIR
在构建时设置并在运行时也设置时,运行时的值将优先。
有关设置开发环境的详细信息,请参阅CONTRIBUTING.md
。
作为预提交钩子设置
Topiary可以无缝集成到pre-commit-hooks.nix:将Topiary作为输入添加到您的flake中,并在pre-commit-hooks.nix的设置中使用
pre-commit-check = nix-pre-commit-hooks.run {
hooks = {
nixfmt.enable = true; ## keep your normal hooks
...
## Add the following:
topiary = topiary.lib.${system}.pre-commit-hook;
};
};
使用方法
Topiary CLI使用多个子命令来区分功能。这些可以通过topiary --help
列出;每个子命令都有自己的专用帮助文本。
CLI app for Topiary, the universal code formatter.
Usage: topiary [OPTIONS] <COMMAND>
Commands:
format Format inputs
visualise Visualise the input's Tree-sitter parse tree
config Print the current configuration
completion Generate shell completion script
help Print this message or the help of the given subcommand(s)
Options:
-C, --configuration <CONFIGURATION>
Configuration file
[env: TOPIARY_CONFIG_FILE]
--configuration-collation <CONFIGURATION_COLLATION>
Configuration collation mode
[env: TOPIARY_CONFIG_COLLATION]
[default: merge]
Possible values:
- merge: When multiple sources of configuration are available, matching items are
updated from the higher priority source, with collections merged as the union of sets
- revise: When multiple sources of configuration are available, matching items
(including collections) are superseded from the higher priority source
- override: When multiple sources of configuration are available, the highest priority
source is taken. All values from lower priority sources are discarded
-v, --verbose...
Logging verbosity (increased per occurrence)
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
格式化
Format inputs
Usage: topiary format [OPTIONS] <--language <LANGUAGE>|FILES>
Arguments:
[FILES]...
Input files and directories (omit to read from stdin)
Language detection and query selection is automatic, mapped from file extensions defined
in the Topiary configuration.
Options:
-t, --tolerate-parsing-errors
Consume as much as possible in the presence of parsing errors
-s, --skip-idempotence
Do not check that formatting twice gives the same output
-l, --language <LANGUAGE>
Topiary language identifier (for formatting stdin)
-q, --query <QUERY>
Topiary query file override (when formatting stdin)
-C, --configuration <CONFIGURATION>
Configuration file
[env: TOPIARY_CONFIG_FILE]
--configuration-collation <CONFIGURATION_COLLATION>
Configuration collation mode
[env: TOPIARY_CONFIG_COLLATION]
[default: merge]
Possible values:
- merge: When multiple sources of configuration are available, matching items are
updated from the higher priority source, with collections merged as the union of sets
- revise: When multiple sources of configuration are available, matching items
(including collections) are superseded from the higher priority source
- override: When multiple sources of configuration are available, the highest priority
source is taken. All values from lower priority sources are discarded
-v, --verbose...
Logging verbosity (increased per occurrence)
-h, --help
Print help (see a summary with '-h')
在从磁盘格式化输入时,语言选择是从输入文件的扩展名检测到的。要格式化标准输入,您必须指定--language
和可选的--query
参数,省略任何输入文件。
注意:fmt
是format
子命令的已知别名。
可视化
Visualise the input's Tree-sitter parse tree
Usage: topiary visualise [OPTIONS] <--language <LANGUAGE>|FILE>
Arguments:
[FILE]
Input file (omit to read from stdin)
Language detection and query selection is automatic, mapped from file extensions defined
in the Topiary configuration.
Options:
-f, --format <FORMAT>
Visualisation format
[default: dot]
Possible values:
- dot: GraphViz DOT serialisation
- json: JSON serialisation
-l, --language <LANGUAGE>
Topiary language identifier (for formatting stdin)
-q, --query <QUERY>
Topiary query file override (when formatting stdin)
-C, --configuration <CONFIGURATION>
Configuration file
[env: TOPIARY_CONFIG_FILE]
--configuration-collation <CONFIGURATION_COLLATION>
Configuration collation mode
[env: TOPIARY_CONFIG_COLLATION]
[default: merge]
Possible values:
- merge: When multiple sources of configuration are available, matching items are
updated from the higher priority source, with collections merged as the union of sets
- revise: When multiple sources of configuration are available, matching items
(including collections) are superseded from the higher priority source
- override: When multiple sources of configuration are available, the highest priority
source is taken. All values from lower priority sources are discarded
-v, --verbose...
Logging verbosity (increased per occurrence)
-h, --help
Print help (see a summary with '-h')
在从磁盘可视化输入时,语言选择是从输入文件的扩展名检测到的。要可视化标准输入,您必须指定--language
和可选的--query
参数,省略输入文件。可视化输出将写入标准输出。
注意:vis
、visualize
和view
是visualise
子命令的已知别名。
配置
Print the current configuration
Usage: topiary config [OPTIONS]
Options:
-C, --configuration <CONFIGURATION>
Configuration file
[env: TOPIARY_CONFIG_FILE]
--configuration-collation <CONFIGURATION_COLLATION>
Configuration collation mode
[env: TOPIARY_CONFIG_COLLATION]
[default: merge]
Possible values:
- merge: When multiple sources of configuration are available, matching items are
updated from the higher priority source, with collections merged as the union of sets
- revise: When multiple sources of configuration are available, matching items
(including collections) are superseded from the higher priority source
- override: When multiple sources of configuration are available, the highest priority
source is taken. All values from lower priority sources are discarded
-v, --verbose...
Logging verbosity (increased per occurrence)
-h, --help
Print help (see a summary with '-h')
请参考下面的配置部分以了解不同的配置来源和合并模式。
注意:cfg
是config
子命令的已知别名。
Shell 完成功能
可以使用completion
子命令生成Topiary的Shell完成脚本。其输出可以根据需要源到您的shell会话或配置文件中。
Generate shell completion script
Usage: topiary completion [OPTIONS] [SHELL]
Arguments:
[SHELL]
Shell (omit to detect from the environment)
[possible values: bash, elvish, fish, powershell, zsh]
Options:
-C, --configuration <CONFIGURATION>
Configuration file
[env: TOPIARY_CONFIG_FILE]
--configuration-collation <CONFIGURATION_COLLATION>
Configuration collation mode
[env: TOPIARY_CONFIG_COLLATION]
[default: merge]
Possible values:
- merge: When multiple sources of configuration are available, matching items are
updated from the higher priority source, with collections merged as the union of sets
- revise: When multiple sources of configuration are available, matching items
(including collections) are superseded from the higher priority source
- override: When multiple sources of configuration are available, the highest priority
source is taken. All values from lower priority sources are discarded
-v, --verbose...
Logging verbosity (increased per occurrence)
-h, --help
Print help (see a summary with '-h')
例如,在Bash中
source <(topiary completion)
日志记录
默认情况下,Topiary CLI只会输出错误消息。您可以通过相应数量的-v
/--verbose
标志来增加日志记录的详细程度
详细程度标志 | 日志记录级别 |
---|---|
无 | 错误 |
-v |
...以及警告 |
-vv |
...以及信息 |
-vvv |
...以及调试输出 |
-vvvv |
...以及跟踪输出 |
退出代码
Topiary过程在成功格式化后将退出并返回零退出代码。否则,定义了以下退出代码
原因 | 代码 |
---|---|
未指定错误 | 1 |
CLI参数解析错误 | 2 |
I/O错误 | 3 |
Topiary查询错误 | 4 |
源解析错误 | 5 |
语言检测错误 | 6 |
幂等性错误 | 7 |
未指定格式化错误 | 8 |
多个错误 | 9 |
当提供多个输入时,即使存在错误,Topiary也会尽力处理所有输入。如果发生任何错误,Topiary将返回非零退出代码。有关这些错误的更多详细信息,请在warn
日志级别下运行Topiary(使用-v
)。
示例
程序构建完成后,可以按以下方式运行
echo '{"foo":"bar"}' | topiary fmt --language json
topiary
还可以通过Cargo或Nix从源代码构建和运行,前提是你已经安装了这些工具
echo '{"foo":"bar"}' | cargo run -- fmt --language json
echo '{"foo":"bar"}' | nix run . -- fmt --language json
它将输出以下格式化的代码
{ "foo": "bar" }
配置
Topiary使用languages.toml
文件进行配置。Topiary最多检查四个位置来寻找此类文件。
配置源
在构建时,此存储库根目录中的languages.toml
嵌入到Topiary中。该文件在运行时被解析。此languages.toml
文件的目的是为Topiary的用户(库和二进制文件)提供合理的默认值。
接下来两个由Topiary二进制文件在运行时读取,允许用户根据需要配置Topiary。第一个是针对用户的,因此可以找到在操作系统的配置目录中
操作系统 | 典型配置路径 |
---|---|
Unix | /home/alice/.config/topiary/languages.toml |
Windows | C:\Users\Alice\AppData\Roaming\Topiary\config\languages.toml |
macOS | /Users/Alice/Library/Application Support/Topiary/languages.toml |
此文件不是由Topiary自动创建的。
下一个源是指定给Topiary的特定项目的设置文件。当在某个目录中运行Topiary时,它将向上遍历文件树,直到找到.topiary
目录。然后,它将读取该目录中存在的任何languages.toml
文件。
最后,可以使用-C
/--configuration
命令行参数(或TOPIARY_CONFIG_FILE
环境变量)显式指定配置文件。这旨在针对非常特定的用例驱动Topiary。
Topiary二进制文件按照以下顺序解析这些源。合并匹配项的操作取决于合并模式。
- 内置配置文件。
- 操作系统配置目录中的用户配置文件。
- 特定项目的Topiary配置。
- 作为CLI参数指定的显式配置文件。
配置选项
配置文件包含一个语言列表,每个语言配置以[[language]]
开头。例如,Nickel的配置如下所示
[[language]]
name = "nickel"
extensions = ["ncl"]
name
字段由Topiary用于将语言条目与查询文件和Tree-sitter语法关联起来。此值应小写书写。每个配置文件中的每个[[language]]
块中的name
字段是必需的。
语言列表对每种语言都是必需的,但并不一定需要在每个配置文件中都存在。对于每种语言,只要有一个配置文件定义了该语言的扩展列表,就足够了。
一个可选的最终字段称为indent
,用于定义该语言的缩进方法。如果Topiary在特定语言的任何配置文件中找不到缩进字段,它将默认使用两个空格" "
。
配置合并
在解析来自多个源配置时,Topiary可以以各种方式合并匹配的配置项(基于语言名称匹配)。合并模式通过--configuration-collation
命令行参数(或TOPIARY_CONFIG_COLLATION
环境变量)设置。
不同的模式最好通过例子来解释。考虑以下两个配置,按优先级从低到高排列(为了说明目的,添加了注释)
# Lowest priority configuration
[[language]]
name = "example"
extensions = ["eg"]
[[language]]
name = "demo"
extensions = ["demo"]
# Highest priority configuration
[[language]]
name = "example"
extensions = ["example"]
indent = " "
合并模式(默认)
匹配项从优先级更高的源更新,集合合并为集合的并集。
# For the "example" language:
# * The collated extensions is the union of the source extensions
# * The indentation is taken from the highest priority source
[[language]]
name = "example"
extensions = ["eg", "example"]
indent = " "
# The "demo" language is unchanged
[[language]]
name = "demo"
extensions = ["demo"]
修订模式
匹配项(包括集合)将从优先级更高的源替换。
# The "example" language's values are taken from the highest priority source
[[language]]
name = "example"
extensions = ["example"]
indent = " "
# The "demo" language is unchanged
[[language]]
name = "demo"
extensions = ["demo"]
覆盖模式
选择优先级最高的源。丢弃所有来自低优先级源的所有值。
# The "example" language's values are taken from the highest priority source
[[language]]
name = "example"
extensions = ["example"]
indent = " "
# The "demo" language does not exist in the highest priority source, so is omitted
设计
只要为一种语言定义了Tree-sitter语法,Tree-sitter就可以解析它并提供一个具体的语法树(CST)。Tree-sitter还将允许我们对这个树进行查询。我们可以利用这一点来定义语言应该如何格式化。以下是一个示例查询
[
(infix_operator)
"if"
":"
] @append_space
这将匹配语法已识别为infix_operator
的任何节点,以及包含if
或:
的任何匿名节点。匹配将使用名称@append_space
捕获。我们的格式化程序会遍历所有匹配和捕获,当我们处理任何名为@append_space
的捕获时,我们将在匹配的节点后添加一个空格。
格式化程序遍历CST节点,并检测所有跨越多行的节点。这被解释为程序员在输入中标记了该节点应该以多行格式化的指示。任何其他节点都将以单行格式化。每当查询匹配插入一个软换行符时,如果节点是多行的,则将其扩展为换行符,如果节点是单行的,则根据是否使用了@append_spaced_softline
或@append_empty_softline
,将其扩展为空格或无。
在呈现输出之前,格式化程序将执行一系列清理操作,例如将连续的空格和新行减少到一行,剪除行尾的空格和前后空白行,以及一致地对缩进和新行指令进行排序。
这意味着例如您可以在if
和true
之前和之后添加空格,我们仍然会输出if true
,单词之间只有一个空格。
支持的捕获指令
这假设您已经熟悉Tree-sitter查询语言。
注意,捕获放在与它相关联的节点之后。如果您想在节点前添加空格,可以这样操作
(infix_operator) @prepend_space
另一方面,这不会起作用
@append_space (infix_operator)
@allow_blank_line_before
如果输入中指定了,将允许匹配的节点之前有一个空白行。对于任何其他节点,将删除空白行。
示例
; Allow comments and type definitions to have a blank line above them
[
(comment)
(type_definition)
] @allow_blank_line_before
@append_delimiter
/ @prepend_delimiter
将匹配的节点追加分隔符。分隔符必须使用谓词!
#delimiter
指定。
示例
; Put a semicolon delimiter after field declarations, unless they already have
; one, in which case we do nothing.
(
(field_declaration) @append_delimiter
.
";"* @do_nothing
(#delimiter! ";")
)
如果已经有一个分号,则将激活@do_nothing
指令并防止查询中的其他指令(这里的@append_delimiter
)应用。否则,";"*
捕获无内容,在这种情况下,关联的指令(@do_nothing
)不会激活。
请注意,当分隔符设置为 " "
(即空格)时,@append_delimiter
与 @append_space
是相同的。
@append_multiline_delimiter
/ @prepend_multiline_delimiter
匹配的节点将被添加一个仅适用于多行的分隔符。这个分隔符只会出现在多行节点中,单行节点中将被省略。分隔符必须使用谓词 #delimiter!
指定。
示例
; Add a semicolon at the end of lists only if they are multi-line, to avoid [1; 2; 3;].
(list_expression
(#delimiter! ";")
(_) @append_multiline_delimiter
.
";"? @do_nothing
.
"]"
.
)
如果已经存在分号,则将激活 @do_nothing
指令,并阻止查询中的其他指令(此处为 @append_multiline_delimiter
)应用。同样,如果节点是单行的,分隔符也不会被添加。
@append_empty_softline
/ @prepend_empty_softline
匹配的节点将被添加或前缀一个空软行。对于多行节点,这将展开为一个换行符,对于单行节点则不展开。
示例
; Put an empty softline before dots, so that in multi-line constructs we start
; new lines for each dot.
(_
"." @prepend_empty_softline
)
@append_hardline
/ @prepend_hardline
匹配的节点将被添加或前缀一个换行符。
示例
; Consecutive definitions must be separated by line breaks
(
(value_definition) @append_hardline
.
(value_definition)
)
@append_indent_start
/ @prepend_indent_start
匹配的节点将在其前后触发缩进。这将仅适用于之后的行,直到收到缩进结束的信号。如果在同一行上开始和结束缩进,则不会发生任何操作。这对于节点无论格式化为单行或多行都能得到正确的行为非常有用。重要的是所有缩进开始和结束都要平衡。
示例
; Start an indented block after these
[
"begin"
"else"
"then"
"{"
] @append_indent_start
@append_indent_end
/ @prepend_indent_end
匹配的节点将触发缩进在其前后结束。
示例
; End the indented block before these
[
"end"
"}"
] @prepend_indent_end
; End the indented block after these
[
(else_clause)
(then_clause)
] @append_indent_end
@append_input_softline
/ @prepend_input_softline
匹配的节点将被添加或前缀一个输入软行。输入软行是如果节点在输入文档中有前导换行符,则是一个换行符;否则是一个空格。
示例
; Input softlines before and after all comments. This means that the input
; decides if a comment should have line breaks before or after. But don't put a
; softline directly in front of commas or semicolons.
(comment) @prepend_input_softline
(
(comment) @append_input_softline
.
[ "," ";" ]* @do_nothing
)
@append_space
/ @prepend_space
匹配的节点将被添加或前缀一个空格。请注意,这与 @append_delimiter
/ @prepend_delimiter
相同,只是分隔符为空格。
示例
[
(infix_operator)
"if"
":"
] @append_space
@append_antispace
/ @prepend_antispace
通常情况下,标记需要与空格相邻,除了少数孤立的环境。为了避免编写复杂的规则来列举每个例外,可以使用 @append_antispace
/ @prepend_antispace
插入一个“反空格”;这将破坏该节点中的所有空格(不是换行符),包括那些由其他格式化规则添加的。
示例
[
","
";"
":"
"."
] @prepend_antispace
@append_spaced_softline
/ @prepend_spaced_softline
匹配的节点将被添加或前缀一个带空格的软行。对于多行节点,这将展开为一个换行符,对于单行节点则展开为一个空格。
示例
; Append spaced softlines, unless there is a comment following.
(
[
"begin"
"else"
"then"
"->"
"{"
";"
] @append_spaced_softline
.
(comment)* @do_nothing
)
@删除
从输出中移除匹配的节点。
示例
; Move semicolon after comments.
(
";" @delete
.
(comment)+ @append_delimiter
(#delimiter! ";")
)
@do_nothing
如果查询中的任何捕获都匹配 @do_nothing
,则匹配将被忽略。
示例
; Put a semicolon delimiter after field declarations, unless they already have
; one, in which case we do nothing.
(
(field_declaration) @append_delimiter
.
";"* @do_nothing
(#delimiter! ";")
)
@multi_line_indent_all
用于注释或其他叶子节点,表示应缩进所有行,而不仅仅是第一行。
示例
(#language! ocaml)
(comment) @multi_line_indent_all
@单行无缩进
匹配的节点将单独打印,占一行,无缩进。
示例
(#language! ocaml)
; line number directives must be alone on their line, and can't be indented
(line_number_directive) @single_line_no_indent
理解不同的换行捕获
类型 | 单行上下文 | 多行上下文 |
---|---|---|
硬行 | 换行 | 换行 |
空软行 | 无 | 换行 |
空格软行 | 空格 | 换行 |
输入软行 | 输入相关 | 输入相关 |
"输入软行"在目标节点后跟换行时作为换行渲染。否则,它们作为空格渲染。
示例
考虑以下JSON,该JSON已被手动格式化以展示不同换行捕获名称操作的每个上下文
{
"single-line": [1, 2, 3, 4],
"multi-line": [
1, 2,
3
, 4
]
}
我们将应用一组简化的JSON格式查询,
- 打开(和关闭)对象的缩进块;
- 每个键值对都占一行,值被拆分到第二行;
- 将不同的换行捕获名称应用于数组分隔符。
也就是说,遍历每个@NEWLINE
类型,我们应用以下
(#language! json)
(object . "{" @append_hardline @append_indent_start)
(object "}" @prepend_hardline @prepend_indent_end .)
(object (pair) @prepend_hardline)
(pair . _ ":" @append_hardline)
(array "," @NEWLINE)
前两条格式规则只是为了清晰起见。最后一条规则是重要的;其结果如下所示
@append_hardline
{
"single-line":
[1,
2,
3,
4],
"multi-line":
[1,
2,
3,
4]
}
@prepend_hardline
{
"single-line":
[1
,2
,3
,4],
"multi-line":
[1
,2
,3
,4]
}
@append_empty_softline
{
"single-line":
[1,2,3,4],
"multi-line":
[1,
2,
3,
4]
}
@prepend_empty_softline
{
"single-line":
[1,2,3,4],
"multi-line":
[1
,2
,3
,4]
}
@append_spaced_softline
{
"single-line":
[1, 2, 3, 4],
"multi-line":
[1,
2,
3,
4]
}
@prepend_spaced_softline
{
"single-line":
[1 ,2 ,3 ,4],
"multi-line":
[1
,2
,3
,4]
}
@append_input_softline
{
"single-line":
[1, 2, 3, 4],
"multi-line":
[1, 2,
3, 4]
}
@prepend_input_softline
{
"single-line":
[1 ,2 ,3 ,4],
"multi-line":
[1 ,2 ,3
,4]
}
自定义作用域和软行
到目前为止,我们已经根据关联的CST节点是否多行将软行扩展为换行。有时,CST节点定义的作用域对于我们的需求太大或太小。例如,考虑以下OCaml代码片段
(1,2,
3)
其CST如下
{Node parenthesized_expression (0, 0) - (1, 2)} - Named: true
{Node ( (0, 0) - (0, 1)} - Named: false
{Node product_expression (0, 1) - (1, 1)} - Named: true
{Node product_expression (0, 1) - (0, 4)} - Named: true
{Node number (0, 1) - (0, 2)} - Named: true
{Node , (0, 2) - (0, 3)} - Named: false
{Node number (0, 3) - (0, 4)} - Named: true
{Node , (0, 4) - (0, 5)} - Named: false
{Node number (1, 0) - (1, 1)} - Named: true
{Node ) (1, 1) - (1, 2)} - Named: false
我们希望在第一个逗号后添加换行,但由于CST结构是嵌套的,包含此逗号的节点(product_expression (0, 1) - (0, 4)
)不是多行,只有顶层节点product_expression (0, 1) - (1, 1)
是多行的。
为了解决这个问题,我们引入用户定义的作用域和软行。
@prepend_begin_scope
/ @append_begin_scope
/ @prepend_end_scope
/ @append_end_scope
这些标签用于定义自定义作用域。结合#scope_id!
谓词,它们定义了可以跨越多个CST节点或只跨越一个部分的作用域。例如,此作用域匹配parenthesized_expression
中的括号之间的任何内容
(parenthesized_expression
"(" @append_begin_scope
")" @prepend_end_scope
(#scope_id! "tuple")
)
作用域软行
我们有四个谓词,在结合#scope_id!
谓词的情况下在自定义作用域中插入软行
@prepend_empty_scoped_softline
@prepend_spaced_scoped_softline
@append_empty_scoped_softline
@append_spaced_scoped_softline
当使用这些作用域内的软线之一时,它们的行为取决于具有相应 scope_id
的最内层包含作用域。如果该作用域是多行的,则软线扩展为换行符。在其他任何方面,它们的行为与其非 scoped
对应物相同。
示例
此 Tree-sitter 查询
(#language! ocaml)
(parenthesized_expression
"(" @begin_scope @append_empty_softline @append_indent_start
")" @end_scope @prepend_empty_softline @prepend_indent_end
(#scope_id! "tuple")
)
(product_expression
"," @append_spaced_scoped_softline
(#scope_id! "tuple")
)
...格式化此代码片段
(1,2,
3)
...为
(
1,
2,
3
)
...同时,单行 (1, 2, 3)
保持不变。
如果我们使用 @append_spaced_softline
而不是 @append_spaced_scoped_softline
,由于它位于单行 product_expression
中,因此 1,
将后跟一个空格而不是换行符。
使用谓词测试上下文
有时,类似于软线的情况,我们希望查询仅在上下文为单行或多行时匹配。Topiary 有几个谓词可以实现此结果。
#single_line_only!
/ #multi_line_only!
这些谓词允许查询仅在匹配的节点处于单行(分别是多行)上下文时触发。
示例
; Allow (and enforce) the optional "|" before the first match case
; in OCaml if and only if the context is multi-line
(
"with"
.
"|" @delete
.
(match_case)
(#single_line_only!)
)
(
"with"
.
"|"? @do_nothing
.
(match_case) @prepend_delimiter
(#delimiter! "| ")
(#multi_line_only!)
)
#single_line_scope_only!
/ #multi_line_scope_only!
这些谓词允许查询仅在包含匹配节点的相关自定义作用域是单行(分别是多行)时触发。
示例
; Allow (and enforce) the optional "|" before the first match case
; in function expressions in OCaml if and only if the scope is multi-line
(function_expression
(match_case)? @do_nothing
.
"|" @delete
.
(match_case)
(#single_line_scope_only! "function_definition")
)
(function_expression
"|"? @do_nothing
.
(match_case) @prepend_delimiter
(#multi_line_scope_only! "function_definition")
(#delimiter! "| ") ; sic
)
建议的工作流程
为了有效地处理查询文件,以下是一种建议的工作方式
-
将一个示例文件添加到
topiary-cli/tests/samples/input
。 -
将相同的文件复制到
topiary-cli/tests/samples/expected
,并对您希望格式化的输出方式进行任何更改。 -
如果是新语言,添加其 Tree-sitter 语法,扩展
crate::language::Language
并在所有地方处理它,然后创建一个几乎为空的查询文件,仅包含(#language!)
配置。 -
运行
RUST_LOG=debug \ cargo test -p topiary-cli \ input_output_tester \ -- --nocapture
如果一切顺利,它应该会输出大量的日志消息。将输出复制到文本编辑器中。您特别感兴趣的是以类似于以下行开始的 CST 输出:
CST node: {Node compilation_unit (0, 0) - (5942, 0)} - Named: true
。💡 作为使用调试输出的替代方法,存在用于输出 Tree-sitter 语法树的
vis
可视化子命令行选项。 -
测试运行将输出实际输出与预期输出之间的所有差异,例如标记之间的空格缺失。选择一个您想修复的差异,并在输入文件中找到行号和列号。
💡 请注意,CST 输出使用基于0的行和列号,因此如果您的编辑器报告第40行,第37列,您可能想选择第39行,第36列。
-
在 CST 调试或可视化输出中,找到该区域的节点,如下所示
[DEBUG atom_collection] CST node: {Node constructed_type (39, 15) - (39, 42)} - Named: true [DEBUG atom_collection] CST node: {Node type_constructor_path (39, 15) - (39, 35)} - Named: true [DEBUG atom_collection] CST node: {Node type_constructor (39, 15) - (39, 35)} - Named: true [DEBUG atom_collection] CST node: {Node type_constructor_path (39, 36) - (39, 42)} - Named: true [DEBUG atom_collection] CST node: {Node type_constructor (39, 36) - (39, 42)} - Named: true
-
这表明您可能想在所有
type_constructor_path
节点之后添加空格(type_constructor_path) @append_space
或者,更有可能的是,您只想在成对的它们之间添加空格
( (type_constructor_path) @append_space . (type_constructor_path) )
或者,也许您想在
constructed_type
的所有子节点之间添加空格(constructed_type (_) @append_space . (_) )
-
再次运行
cargo test
,查看输出是否有所改善,然后返回到步骤5。
语法树可视化
为了支持格式化查询的开发,可以使用 --visualise
命令行选项生成给定输入的 Tree-sitter 语法树。
这目前支持 JSON 输出,包含与调试输出相同的信息,以及 GraphViz DOT 输出,这对于生成语法图很有用。(注意,可视化输出的文本位置序列是1-based,与调试输出的0-based位置不同。)
基于终端的游乐场
Nix 用户可能还会发现 playground.sh
脚本有助于辅助查询文件的交互式开发。在终端中运行时,它将使用请求的查询文件格式化给定的源输入,并在任何针对这些文件的 inotify 事件发生时更新输出。
Usage: ${PROGNAME} LANGUAGE [QUERY_FILE] [INPUT_SOURCE]
LANGUAGE can be one of the supported languages (e.g., "ocaml", "rust",
etc.). The packaged formatting queries for this language can be
overridden by specifying a QUERY_FILE.
The INPUT_SOURCE is optional. If not specified, it defaults to trying
to find the bundled integration test input file for the given language.
例如,可以在 tmux 窗格中运行游乐场,同时打开另一个编辑器。
相关工具
Tree-Sitter 特定
- 语法树游乐场:一个用于实验 Tree-sitter 和其查询语言的交互式在线游乐场。
- Neovim Treesitter 游乐场:一个用于 Neovim 的 Tree-sitter 游乐场插件。
- Difftastic:一个利用 Tree-sitter 进行语法比较的工具。
元和多语言格式化器
- format-all:一个用于 Emacs 的格式化器协调器。
- null-ls.nvim:一个用于 Neovim 的 LSP 框架,它简化了格式化器的协调。
- prettier:一个支持多种(与 Web 开发相关的)语言的格式化器。
- treefmt:一个通用格式化器协调器,它将格式化器统一在公共接口下。
相关格式化器
- gofmt:Go 的既定标准格式化器,也是我们格式化器风格的灵感来源。
- ocamlformat:OCaml 的格式化器。
- ocp-indent:一个用于缩进 OCaml 代码的工具。
- Ormolu:我们的 Haskell 格式化器,其设计原则与 Topiary 类似。
- rustfmt:Rust 的既定标准格式化器。
- shfmt:Bash 等的解析器、格式化器和解释器。
依赖关系
~4–8MB
~137K SLoC