1 个不稳定版本

0.4.0 2024年5月17日

数据库接口 中排名 887

Download history 146/week @ 2024-05-17 13/week @ 2024-05-24 7/week @ 2024-05-31 9/week @ 2024-06-07 13/week @ 2024-06-14 2/week @ 2024-06-21 3/week @ 2024-06-28 13/week @ 2024-07-05 12/week @ 2024-07-12 5/week @ 2024-07-19 42/week @ 2024-07-26 33/week @ 2024-08-02 17/week @ 2024-08-09

每月下载量 99
5 个 Crates 中使用 (2 个直接使用)

Apache-2.0 WITH LLVM-exception

75KB
731

Topiary

Latest Release CI Status Discord

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

作为pre-commit钩子设置

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 参数,省略任何输入文件。

注意: fmtformat 子命令的已知别名。

可视化

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 参数,省略输入文件。可视化输出将写入标准输出。

注意: visvisualizeviewvisualise 子命令的已知别名。

配置

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')

请参阅下面的 配置 部分,了解不同的配置源和归并模式。

注意: cfgconfig 子命令的已知别名。

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 将仅输出错误消息。您可以使用相应数量的 -/--verbose 标志来增加日志详细程度

详细程度标志 日志级别
错误
-v ...以及警告
-vv ...以及信息
-vvv ...以及调试输出
-vvvv ...以及跟踪输出

退出代码

当成功格式化时,Topiary 进程将使用零退出代码退出。否则,定义了以下退出代码

原因 代码
未指定错误 1
CLI 参数解析错误 2
I/O 错误 3
Topiary 查询错误 4
源解析错误 5
语言检测错误 6
幂等性错误 7
未指定格式化错误 8
多个错误 9

当给出多个输入时,即使出现错误,Topiary 也将尽力处理所有输入。如果发生任何错误,Topiary 将返回非零退出代码。有关这些错误性质的更多详细信息,请在 warn 日志级别(带 -)下运行 Topiary。

示例

构建完成后,程序可以像这样运行

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。第一个旨在用于特定用户,因此可以找到在 OS 的配置目录中

OS 典型配置路径
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 二进制文件按以下顺序解析这些来源。对匹配项进行合并的操作取决于 合并模式

  1. 内置配置文件。
  2. 在操作系统的配置目录中的用户配置文件。
  3. 特定项目的 Topiary 配置。
  4. 作为 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,它将被扩展为一个空格或无内容。

在渲染输出之前,格式化器将执行多项清理操作,例如将连续的空格和新行缩减为一个,修剪行尾的空格以及首尾的空白行,并一致地排序缩进和换行指令。

这意味着例如,你可以在 iftrue 前后添加空格,我们仍然会输出 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

@single_line_no_indent

匹配的节点将以单行打印,没有缩进。

示例

(#language! ocaml)
; line number directives must be alone on their line, and can't be indented
(line_number_directive) @single_line_no_indent

理解不同的换行符捕获

类型 单行上下文 多行上下文
硬行 换行符 换行符
空软行 换行符
带空格的软行 空格 换行符
输入软行 输入相关 输入相关

"输入软行"在目标节点后跟换行符时将被渲染为新行。否则,它们将被渲染为空格。

示例

考虑以下JSON,它已经被手动格式化为展示不同换行捕获名称在哪些上下文中运行

{
  "single-line": [1, 2, 3, 4],
  "multi-line": [
    1, 2,
    3
    , 4
  ]
}

我们将应用一组简化的JSON格式查询,

  1. 为对象打开(并关闭)缩进的块;
  2. 每个键值对都占一行,值被分割到第二行;
  3. 在数组分隔符上应用不同的换行捕获名称。

也就是说,遍历每个 @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,则 1, 后面将跟一个空格而不是换行符,因为它位于一个单行 product_expression 中。

测试与谓词相关的上下文

有时,与软行类似,我们希望查询仅在上下文为单行或多行时匹配。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
)

建议的工作流程

为了有效地处理查询文件,以下是一个建议的工作方式

  1. 将一个示例文件添加到 topiary-cli/tests/samples/input

  2. 将相同的文件复制到 topiary-cli/tests/samples/expected,并根据您希望输出如何格式化的方式做出更改。

  3. 如果是新语言,添加其 Tree-sitter 语法,扩展 crate::language::Language 并在所有地方处理它,然后创建一个几乎为空的查询文件,只包含 (#language!) 配置。

  4. 运行

    RUST_LOG=debug \
    cargo test -p topiary-cli \
               input_output_tester \
               -- --nocapture
    

    如果一切正常,它应该输出大量的日志消息。将输出复制到文本编辑器中。您特别关注以类似以下行开始的 CST 输出:CST node: {Node compilation_unit (0, 0) - (5942, 0)} - Named: true

    💡 作为使用调试输出的替代方案,存在 vis 可视化子命令行选项,可以以各种格式输出 Tree-sitter 语法树。

  5. 测试运行将输出实际输出和预期输出之间的所有差异,例如,标记之间的缺失空格。选择一个您想要修复的差异,并找到输入文件中的行号和列号。

    💡 请记住,CST 输出使用基于 0 的行号和列号,因此如果您的编辑器报告第 40 行,第 37 列,您可能想要第 39 行,第 36 列。

  6. 在 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
    
  7. 这表明您可能希望所有 type_constructor_path 节点后面都有空格

    (type_constructor_path) @append_space
    

    或者,更可能的是,您只是希望在它们之间添加空格

    (
      (type_constructor_path) @append_space
      .
      (type_constructor_path)
    )
    

    或者,也许您希望 constructed_type 的所有子节点之间都有空格

    (constructed_type
      (_) @append_space
      .
      (_)
    )
    
  8. 再次运行 cargo test,以查看输出是否有所改善,然后返回到步骤 5。

语法树可视化

为了支持格式化查询的开发,可以使用--visualise命令行选项来生成给定输入的Tree-sitter语法树。

目前支持JSON输出,涵盖与调试输出相同的信息,以及GraphViz DOT输出,这对于生成语法图很有用。(注意:可视化输出的文本位置序列化是1开始的,与调试输出的0开始的位置不同。)

基于终端的游乐场

Nix用户可能会发现playground.sh脚本能帮助他们在终端中交互式地开发查询文件。在终端中运行时,它将使用请求的查询文件格式化给定的源输入,并在针对这些文件的通知事件发生时更新输出。

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特定

元和多语言格式化器

  • 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等语言的解析器、格式化器和解释器。

依赖项

~9.5MB
~178K SLoC