基于tree-sitter的结构性编辑插件

https://github.com/manateelazycat/grammatical-edit 是基于 tree-sitter 的结构化编辑插件, 这个项目的目标是实现 ParEdit、thing-edit、textobj 这三类插件的功能合集。

最主要的优点是基于 tree-sitter 的 AST 而不是正则来实现语法对象编辑,只要 tree-sitter 支持的编程语言,都可以进行结构性编辑,不需要像 smartparens 或者 awesome-pair.el 针对一些偏门语言增加 workaround, 甚至是明天才发布的新编程语言都可以支持。

安装方法

  1. 先安装 tree-sitter: https://emacs-tree-sitter.github.io/installation/
  2. 下载 grammatical-edit 后在配置文件中加入(require 'grammatical-edit)

配置启用的语言

(dolist (hook (list
               'c-mode-common-hook
               'c-mode-hook
               'c++-mode-hook
               'java-mode-hook
               'haskell-mode-hook
               'emacs-lisp-mode-hook
               'lisp-interaction-mode-hook
               'lisp-mode-hook
               'maxima-mode-hook
               'ielm-mode-hook
               'sh-mode-hook
               'makefile-gmake-mode-hook
               'php-mode-hook
               'python-mode-hook
               'js-mode-hook
               'go-mode-hook
               'qml-mode-hook
               'jade-mode-hook
               'css-mode-hook
               'ruby-mode-hook
               'coffee-mode-hook
               'rust-mode-hook
               'qmake-mode-hook
               'lua-mode-hook
               'swift-mode-hook
               'minibuffer-inactive-mode-hook
               ))
  (add-hook hook '(lambda () (grammatical-edit-mode 1))))

快捷键配置

(define-key grammatical-edit-mode-map (kbd "(") 'grammatical-edit-open-round)
(define-key grammatical-edit-mode-map (kbd "[") 'grammatical-edit-open-bracket)
(define-key grammatical-edit-mode-map (kbd "{") 'grammatical-edit-open-curly)
(define-key grammatical-edit-mode-map (kbd ")") 'grammatical-edit-close-round)
(define-key grammatical-edit-mode-map (kbd "]") 'grammatical-edit-close-bracket)
(define-key grammatical-edit-mode-map (kbd "}") 'grammatical-edit-close-curly)
(define-key grammatical-edit-mode-map (kbd "=") 'grammatical-edit-equal)

(define-key grammatical-edit-mode-map (kbd "%") 'grammatical-edit-match-paren)
(define-key grammatical-edit-mode-map (kbd "\"") 'grammatical-edit-double-quote)

(define-key grammatical-edit-mode-map (kbd "SPC") 'grammatical-edit-space)
(define-key grammatical-edit-mode-map (kbd "RET") 'grammatical-edit-newline)

(define-key grammatical-edit-mode-map (kbd "M-o") 'grammatical-edit-backward-delete)
(define-key grammatical-edit-mode-map (kbd "C-d") 'grammatical-edit-forward-delete)
(define-key grammatical-edit-mode-map (kbd "C-k") 'grammatical-edit-kill)

(define-key grammatical-edit-mode-map (kbd "M-\"") 'grammatical-edit-wrap-double-quote)
(define-key grammatical-edit-mode-map (kbd "M-[") 'grammatical-edit-wrap-bracket)
(define-key grammatical-edit-mode-map (kbd "M-{") 'grammatical-edit-wrap-curly)
(define-key grammatical-edit-mode-map (kbd "M-(") 'grammatical-edit-wrap-round)
(define-key grammatical-edit-mode-map (kbd "M-)") 'grammatical-edit-unwrap)

(define-key grammatical-edit-mode-map (kbd "M-p") 'grammatical-edit-jump-right)
(define-key grammatical-edit-mode-map (kbd "M-n") 'grammatical-edit-jump-left)
(define-key grammatical-edit-mode-map (kbd "M-:") 'grammatical-edit-jump-out-pair-and-newline)

添加 tree-sitter 对 elisp 的支持

1. git clone https://github.com/Wilfred/tree-sitter-elisp
2. gcc ./src/parser.c -fPIC -I./ --shared -o elisp.so
3. cp ./elisp.so ~/.tree-sitter-langs/bin
(tree-sitter-load 'elisp "elisp")
(add-to-list 'tree-sitter-major-mode-language-alist '(emacs-lisp-mode . elisp))

添加Vue对tree-sitter支持

1. git clone https://github.com/ikatyang/tree-sitter-vue.git
2. gcc ./src/parser.c ./src/scanner.cc -fPIC -I./ --shared -o vue.so
3. cp ./vue.so ~/.tree-sitter-langs/bin (~/.tree-sitter-langs/bin is path of your tree-sitter-langs repo)
(tree-sitter-load 'vue "vue")
(add-to-list 'tree-sitter-major-mode-language-alist '(web-mode . vue))

备注

grammatical-edit的代码 fork 于 awesome-pair.el , 仍然有一部分代码需要从原来正则匹配转换成 AST 逻辑,欢迎大家试用反馈。

11 个赞

masteringemacs 也用 treesitter 做了一个类似的东西,这大概就是英雄所见略同把

https://www.masteringemacs.org/article/tree-sitter-complications-of-parsing-languages https://github.com/mickeynp/combobulate

https://github.com/manateelazycat/grammatical-edit/commit/54830880ab04863ebf743649a998401a91896299

用tree-sitter AST 重写了 grammatical-edit-web-mode-kill ,全部用语法对象来判断,不但代码比原来一堆正则解析更简单,功能也更加稳定了。

向lazycat再分享一个基于tree-sitter 的search https://github.com/BrianHicks/tree-grepper

1 个赞

这个插件主要是搜索结构模式? 主要用在哪些场景?

是的这个插件只关心结构搜索

就像作者博客里写到的 https://bytes.zone/posts/tree-grepper/ (可以看看作者的场景) Tree-grepper is focused only on search: it doesn't do linting (like semgrep) or AST-based refactoring (like comby.)

具体说有什么其他应用场景,就需要大家一起来探索脑洞了,简单来讲tree-greeper就是基于Tree-sitter’s s-expressions query的进一步增强。

感觉可以做为结构模式搜索去研究重复代码,但是感觉没人眼看快啊,哈哈哈哈

  1. 他可以找到一个变量的类型,进而增强tag系统的能力。

  2. 可以很快的了解一个类的某个方法是在哪里及如何被用到的。

重构的差不多了,现在 vue.js 这种 web-mode kill tag的操作简直幸运流水, tree-sitter AST API把我原来大量的正则匹配的方法全部替换掉了,而且代码非常稳定和通用。

2 个赞

终于知道怎么添加额外的tree-sitter lang了

Windows 10 + MingW64 gcc ./src/parser.c -fPIC -I./ -I./src/ --shared -o elisp.dll

需要 -I./src/

1 个赞

今年 emacsconf2021 上也有一个对应的

https://emacsconf.org/2021/talks/structural/

1 个赞

我看了这个视频,目前他大量用 wrap 和 unwrap 的操作,除了 Lisp 语言外,其实 wrap 和 unwrap 的频率没有那么高。

平常最常用的应该是 kill 和 backward-delete 的操作, 特别是 kill 要连续识别光标后的各种语法情况。

我自己下载下来试了一下,目前从实战角度来看,grammatical-edit 最接近 paredit 的操作,逻辑严谨性上比 awesome-pair.el 更好。

我也在win10上编译了elisp.dll,但执行(tree-sitter-load 'elisp “elisp”)报错,错误信息是:Debugger entered–Lisp error: (tsc-lang-load-failed “LoadLibraryExW failed”) tsc–load-language(“d:/Users/home/.emacs.d/elpa/tree-sitter-langs-2021…” “tree_sitter_elisp” elisp) #f(compiled-function (lang-symbol &optional file native-symbol-name) “Load a language grammar from FILE and register it under the name LANG-SYMBOL.\nIf another language was already registered under the same name, override it.\n\nThis function returns the loaded language object.\n\nFILE should be the base name (without extension) of the native shared library\nthat exports the language as the native symbol NATIVE-SYMBOL-NAME.\n\nIf FILE is nil, the base name is assumed to be LANG-SYMBOL’s name.\n\nIf NATIVE-SYMBOL-NAME is nil, the name of the exported native symbol is assumed\nto be LANG-SYMBOL’s name, prefixed with “tree_sitter_”.” #<bytecode 0x13dfd890fec6abca>)(elisp “elisp”) 感觉好像是这个dll有问题。我是在cygwin上用mingw64-gcc的tool-chain编译的,不知道是不是这样编出来不能用?

1 个赞

你的emacs是用什么编译的,也是cygwin?

GNU Emacs 28.0.50 (build 1, x86_64-w64-mingw32) of 2021-01-16

我的是GNU Emacs 29.0.50 (build 1, x86_64-w64-mingw32) of 2021-11-17

elisp.dll使用 msys2的mingw64编译的,emacs也是

好像我的编译工具版本有问题,我再重新下载一下试试

请教下这个跟 https://github.com/ethan-leba/tree-edit 类似吗?

要比 tree-edit 成熟很多,tree-sitter 只是提供AST类型信息,但是真正要做到 paredit 实战级别,要做很多边界处理。

grammatical-edit 最近的 Git commit 可以看出来,即使 tree-sitter 提供AST类型信息的情况下,光删除字符串在 golang、python、rust、web、c++ 不同语言都有非常多的细节化处理。

tree-edit 目前更多是在演示不同AST结构的快速范围选择,但是在细粒度实战方面,目前还没有看到有这方面的代码提交。

1 个赞

非常感谢! :star_struck: