vscode 声明式树遍历的缩进规则

g0czyy6m  于 8个月前  发布在  Vscode
关注(0)|答案(4)|浏览(206)

作为VS Code团队正在考虑将tree-sitter集成到UI线程( #161479 )中,以提高标记化( #77140 )和语法高亮( #50140 )的可能性,我想建议的是缩进是另一个可以通过tree-sitter大幅改进的领域。
目前,当涉及到定义缩进时,语言扩展具有以下可能性,但都有其缺点:

  • 定义 indentationRulesonEnterRules 正则表达式。由于正则表达式在匹配编程语言构造方面的限制,这在许多边缘情况下会产生令人惊讶的缩进,而这些情况通常无法完全概括地修复。例如,Python扩展就在这方面遇到了不少问题: Improve auto-indentation behaviour vscode-python#481
  • 通过覆盖Enter键来绕过基于正则表达式的缩进,例如:

https://marketplace.visualstudio.com/items?itemName=KevinRose.vsc-python-indent
然后扩展就可以维护代码结构的知识和实现一个临时引擎。
这种方法只有在按下Enter键后进行缩进时才有效,移动行上下或粘贴时不会调用缩进命令。此外,如果扩展主机被压垮,那么在按下Enter键和在编辑器中插入换行符和缩进之间可能会有明显的延迟。

我称之为“纠正”,因为渲染器线程的缩进规则首先被应用并立即可见。如果扩展主机响应缓慢,格式化编辑将在用户可见的情况下被应用第二次。这还不算太糟糕,但如果用户输入得太快并且使发送给提供者的文档版本无效,格式请求也可能会被取消/忽略。
在渲染器进程和通过格式化类型进行编辑纠正方面进行缩进是一个总体上的好妥协,但它需要非平凡的分析和在扩展代码中实现的行为。允许扩展提供更智能的、了解代码结构树的缩进规则(并带有垂直对齐的规定,参见 #66235 )将使UI线程中的初始缩进在一般情况下更加准确。
这就是tree-sitter的作用所在。Emacs和nvim都提供了由声明性tree-sitter查询指定的缩进引擎:

文档: https://www.gnu.org/software/emacs/manual/html_node/elisp/Parser_002dbased-Indentation.html

  • nvim文档:https://neovim.io/doc/user/treesitter.html#treesitter-query

https://github.com/nvim-treesitter/nvim-treesitter/blob/aa8d8bc600e00f84d11b9d40c6900d72d0f68fa3/doc/nvim-treesitter.txt#L210
C示例 https://github.com/nvim-treesitter/nvim-treesitter/blob/master/queries/c/indents.scm
同样,VS Code的一个tree-sitter声明性用于缩进的语言也可以解决仅在主进程中运行完全基于树的复杂缩进而不运行扩展代码的问题。

omqzjyyz

omqzjyyz1#

你刚才是在做类似这样的工作吗?

bjg7j2ky

bjg7j2ky2#

你好,@lionel-@alexr00,感谢你提出这个问题。确实,我们目前正在讨论使用tree-sitter来执行缩进。目前,我们正试图改进缩进的正则表达式规则并探索这些规则的极限。正如你提到的,大多数情况下,由于缩进代码不了解上下文,所以正则表达式缩进规则表现不佳。这就是为什么tree-sitter可能是一个很好的替代方案。使用tree-sitter的一个问题是,它将计算缩进更加耗时,并且缩进将异步计算(而当前计算是同步的)。这意味着与format-on-type一样,缩进存在延迟。我们将继续团队内部讨论这个功能请求。

pb3s4cty

pb3s4cty3#

使用tree-sitter的一个问题是,它在进行缩进计算时会更加耗费计算资源,并且缩进计算是异步进行的(而当前计算是同步的)。这意味着缩进与格式化类型一样存在延迟。我们将继续团队内部讨论这个功能请求。
有趣的是,我希望TS的增量重解析能够足够快以同步完成所有操作。据我所知,异步性仅限于渲染器进程,不会受到扩展主机减速的影响?希望大多数时候延迟不会引起注意。
我们正在努力改进缩进正则表达式规则并探索这些规则的极限。正如您提到的,正则表达式缩进规则性能不佳的主要原因是缩进代码无法了解上下文。这就是为什么tree-sitter可能是一个很好的替代方案。
在此我想提一下我们的一个重要用例(在#136593中讨论过):由操作符分隔的表达式链,如a && b && c。在语言R中,这些链(通常称为管道)非常常见:

  1. # ggplot2 pipeline
  2. mtcars |>
  3. ggplot(aes(
  4. disp,
  5. drat
  6. )) +
  7. geom_point(
  8. aes(colour = cyl)
  9. )
  10. # dplyr pipeline
  11. starwars |>
  12. group_by(species) |>
  13. summarise(
  14. n = n(),
  15. mass = mean(mass, na.rm = TRUE)
  16. ) |>
  17. filter(
  18. n > 1,
  19. mass > 50
  20. )

我们目前使用的是OnEnter规则,旨在避免类似楼梯式的缩进效果:

  1. # Unwanted staircase
  2. foo() +
  3. bar() +
  4. baz()
  5. # Expected indentation
  6. foo() +
  7. bar() +
  8. baz()

然而,这些规则只能真正处理一行代码,当管道元素分布在多行时,它们开始失效:

  1. foo() +
  2. bar(
  3. x
  4. ) +
  5. baz(
  6. y
  7. )

即使我们可以跨越更多行进行匹配,我认为我们也无法完全用正则表达式表达我们需要的一般性规则。
使用nvim tree-sitter查询,我们可以用以下方式简单地表示这种对齐规则:

  1. [
  2. (binary_operator)
  3. ] @indent.begin

这应该适用于具有相同左结合运算符的情况,因为在那种情况下,由于我们是从右侧进行缩进,当前节点的父节点将是运算符链中最顶层的节点。
对于更复杂的情况,如右结合运算符,我们可以使用Emacs的方法来表示缩进:使用匹配器和锚点。首先,以下是上面提到的相同的nvim规则:

  1. ((parent-is "binary_operator") parent-bol r-ts-mode-indent-offset)

这匹配二元运算符的子节点并从父节点进行缩进。为了确保我们总是从二元运算符链的最顶层节点进行缩进,我们可以修改如下:

  1. ((parent-is "binary_operator") (ancestor-while "binary_operator") r-ts-mode-indent-offset)

这使用了一个锚点,只要类型匹配,就会沿着树向上爬行,并选择最后一个匹配项的起始位置:

  1. (defun ancestor-while (type)
  2. (lambda (node parent _bol &rest _)
  3. (while (and parent (string-match-p type (treesit-node-type parent)))
  4. (setq node parent)
  5. (setq parent (treesit-node-parent parent)))
  6. (treesit-node-start node)))

这个例子说明了TS规则如何表达基于正则表达式的方法难以处理的缩进行为。我们需要一个足够灵活的API来表达更复杂的缩进规则,但应该很容易根据需要扩展API以添加其他匹配器和锚点。

展开查看全部
zxlwwiss

zxlwwiss4#

你好@lionel- ,感谢分享这些有趣的想法。遗憾的是,到目前为止,我不清楚我们是否会考虑更改缩进API。如果有这个计划,我会告诉你,并考虑你的想法。
关于异步性,我认为树解析将在单独的工作线程中完成。延迟可能很小,因此正如你提到的那样并不明显 - 我们必须测试以确认这一点。

相关问题