tree-sitter 好像基本完成了?

https://lists.gnu.org/archive/html/emacs-devel/2022-01/msg00410.html

作者主页的安装文档 https://archive.casouri.cat/note/2021/emacs-tree-sitter/index.html

大致试着安装了一下:

  1. 首先安装下 tree-sitter 的库
git clone https://github.com/tree-sitter/tree-sitter.git --single-branch  --depth=1
cd tree-sitter
make
make install

注:我是 debian 11,默认将生成的库文件放在了/usr/local/lib 目录下,如果你也跟我一样编译 Emacs的时候找不到 libtree-sitter.so 文件的话,可以尝试设置下LD_LIBRARY_PATH 或者直接在 /usr/lib/x86_64-linux-gnu 下建立两个软链接,链接到 libtree-sitter.so.0.0或者直接复制过去就可以编译成功了,编译成功的话就不需要下面这步了。

sudo ln -s /usr/local/lib/libtree-sitter.so.0.0 libtree-sitter.so.0
sudo ln -s /usr/local/lib/libtree-sitter.so.0.0 libtree-sitter.so
  1. 编译 emacs
git clone https://github.com/casouri/emacs.git --single-branch  --depth=1 --branch ts 
./autogen.sh
./configure --with-tree-sitter
make

PS: 按作者主页的说法,编译的时候会自动检测,不需要加 --with-tree-sitter 也可以。

  1. 获取一些语言的定义?

    作者写了个脚本自动获取、编译,很方便

git clone https://github.com/casouri/tree-sitter-module.git
cd tree-sitter-module
./batch-new.sh

编译成功后会将生成的文件放置在 dist 目录下,将这些文件放在比如 /usr/lib/x86_64-linux-gnu 目录下,当然,你也可以建一个软链接链接过来,或者设置下 LD_LIBRARY_PATH 都可以。

  1. 查看文档 除了作者主页的文档外,还可以查看内置的文档,打开你刚刚编译好的 emacs,然后输入
C-h i m elisp RET g Parsing Program Source RET

好了…

  1. 其他

恩嗯嗯… 作者安装语言定义的脚本里只有 C, JSON, Go, HTML, JavaScript, CSS and Python 这几个语言,你可以通过使用 build-new 这个脚本安装

这个仓库里所支持的所有语言,比如安装 javascript:

./build-new javascript

不再这个仓库里的可以自己修改一下 build-new 这个脚本,很简单:

lang=$1 这行下面添加 address=$2,把 # Retrieve sources. 这行下面的

git clone "https://github.com/tree-sitter/tree-sitter-${lang}.git" \
    --depth 1

修改成

if [ "$#" == "2" ]; then
     git clone $address --depth 1
else
    git clone "https://github.com/tree-sitter/tree-sitter-${lang}.git" \
    --depth 1
fi

例子:添加 elisp:

./build-new.sh elisp https://github.com/Wilfred/tree-sitter-elisp.git

两个参数,第二个参数是地址,第一个参数是语言名,就是地址 tree-sitter- 后面的那个

6 个赞

就等合并然后更新了

哪个版本?29?

tree sitter 分支中的提交记录令人担忧,可能需要重写 commit message 才能合并。

1 个赞

还没有合并呢

有没有人介绍一下, 它在emacs里的工作原理? 比如用不用text property? 一开始是全buffer解析吗?

tree-sitter好像占用内存是原始文件的好几倍, 可能需要一些优化, 比如使用类似折叠的思路, 先全buffer解析一下, 保存一个轮廓(类似outline), 然后具体编辑的地方或者用到的地方, 再进行局部详细解析, 这样内存占用量会大大减少.

是不是全文解析取决于使用者,我这里介绍几个 api

  1. 创建一个 parser,比如 html 的。
(require 'tree-sitter)
;;; 下面三个都是创建一个 parser
;;; 创建一个 parser,已经有的话,返回现成的
(tree-sitter-get-parser-create 'tree-sitter-html)
;;; 总是创建一个新的 parser
(tree-sitter-get-parser  'tree-sitter-html)
;;; 给某个 buffer 创建 parser,第一个参数为 nil 的话,默认当前 buffer
(tree-sitter-parser-create nil 'tree-sitter-html)
;;; 或者保存在一个变量里
(setq-local tree-sitter-html (tree-sitter-parser-create nil 'tree-sitter-html))
  1. 进行解析,是否解析,什么时候解析都取决于使用者,buffer 变动的时候 treesitter 是不会自动更新的,当然,你可以自己设置。
tree-sitter-node-at beg &optional end parser-or-lang named
(tree-sitter-node-at (point))
;;;解析当前位置的这个节点,默认调用已经定义的 parser,beg、 end 是解析的范围, parser-or-lang 是调用的 parser,即你可以在一个 buffer 调用多个 parser, named 就是一个节点的名字(在语法树里的名字)

(tree-sitter-node-at (point) (point) (tree-sitter-get-parser-create 'tree-sitter-javascript))
;;; 在当前节点调用 javascript 的 parser

(tree-sitter-parser-root-node tree-sitter-html)
;;;解析整个文档

tree-sitter-parse-string string language
;;; 解析一个字符串

其他的后面再补,文档我还没有看完…

1 个赞
  1. 通过现有节点查询其他节点
;;; tree-sitter-node-parent node
;;; 查询当前节点的父节点
(tree-sitter-node-parent (tree-sitter-node-at (point)))

;;;tree-sitter-node-child node n &optional named
;;; 查询孩子节点
(tree-sitter-node-child (tree-sitter-node-at (point-at-bol) (point-at-eol)) 1)

;;; tree-sitter-node-children node &optional named
;;; 所有的孩子节点以列表的形式返回
(tree-sitter-node-children (tree-sitter-node-at (point-at-bol) (point-at-eol)))

;;;tree-sitter-next-sibling node &optional named
;;;tree-sitter-prev-sibling node &optional named
;;; 查询前后兄弟节点

  1. 节点有了,现在查看节点的具体信息
;;; 节点的开始位置
(tree-sitter-node-start (tree-sitter-node-at (point)))
;;; 节点的结束位置
(tree-sitter-node-end (tree-sitter-node-at (point)))
;;; 节点的内容
(tree-sitter-node-text (tree-sitter-node-at (point)))
;;; 返回 node 的类型
(tree-sitter-node-type (tree-sitter-node-at (point)))
  1. 下面在介绍一下 tree-sitter 处理多语言,相关 api 有好几个,我稍微介绍一个:
;;; tree-sitter-parser-set-included-ranges parser ranges
;;; ranges 的形式为由 (BEG . END) 组成的 list
;;; 例子:
(tree-sitter-parser-set-included-ranges
           parser '((1 . 9) (16 . 24) (24 . 25)))

tree-sitter 的 api 非常的丰富,我上面的只是其中的一小部分,各种查询,验证,模式查询什么的应有尽有,我目前了解的还是比较浅的,具体的可以自己去看看。

1 个赞

原来如此, 有比较大的可玩空间.

对,这些都是比较底层的 api,更上一点的比如语法高亮和缩进,缩进的话作者也有专门准备了一些 api,语法高亮貌似作者直接写了函数tree-sitter-font-lock-enable 对接了自带的 font-lock

不是,他们用的是elisp-tree-sitter 这个包,他是通过dynamic module 实现的对 Emacs 的支持。

坐等 tree-sitter 合并到 master 分支了 :grinning:

如果没人重写提交记录可能不会合并,GNU 项目要求所有提交信息都是有效的 ChangeLog entry,详情可以参考 CONTRIBUTE 和 https://www.gnu.org/prep/standards/html_node/Change-Logs.html (standards)Change Logs

2 个赞

作者编写的脚本只支持 7 个语言,你可以将作者编写的脚本下载下来之后,在 tree-sitter-module 目录下新建一个文件,起个名字比如:build-my.sh 之类的,然后,把下面的代码复制进去,之后给文件加上执行权限:chmod +x build-my.sh

之后你可以执行 ./build-my.sh help 来查看该脚本的使用方法以及支持的语言,该脚本的使用很简单:

  1. 你直接执行 ./build-my.sh 它会默认构建所有它支持的语言。

  2. 你执行 ./build-my.sh language 对支持的语言进行构建,比如 ./build-my.sh c,那么他将会构建对 C 的支持

  3. ./build-my.sh language url 对不支持的语言,通过添加 url 来进行构建。当然,前提是对方的语言规范编写符合要求。例子:./bash-my.sh make https://github.com/alemuller/tree-sitter-make

  4. ./build-my.sh bash ./build-my.sh bash c ./build-my.sh bash c cpp 以此类推,这种的,会对 ./build-my.sh 后面跟的语言进行构建,当然前提是本脚本支持。

PS:这个脚本基本上是在作者的 build-new.sh 上改出来的。

#!/bin/bash
# tree-sitter github 官网上支持的语言
languages=(
    'bash'
    'c'
    'cpp'
    'css'
    'c-sharp'
    'go'
    'html'
    'haskell'
    'java'
    'javascript'
    'json'
    'julia'
    'python'
    'php'
    'ql'
    'ruby'
    'rust'
    'regex'
    'scala'
    'swift'
)
# 非 tree-sitter 官网上收录的语言
languages_other=(
    'commonlisp'
    'cuda'
    'elisp'
    'lua'
    'make'
    'markdown'
    'objc'
    'perl'
    'r'
    'sql'
    'toml'
    'tsx'
    'typescript'
    'vue'
)
declare -A languages_other_url
languages_other_url["toml"]="https://github.com/ikatyang/tree-sitter-toml"
languages_other_url["sql"]="https://github.com/m-novikov/tree-sitter-sql"
languages_other_url["perl"]="https://github.com/ganezdragon/tree-sitter-perl"
languages_other_url["objc"]="https://github.com/merico-dev/tree-sitter-objc"
languages_other_url["commonlisp"]="https://github.com/theHamsta/tree-sitter-commonlisp"
languages_other_url["vue"]="https://github.com/ikatyang/tree-sitter-vue"
languages_other_url["elisp"]="https://github.com/Wilfred/tree-sitter-elisp"
languages_other_url["lua"]="https://github.com/Azganoth/tree-sitter-lua"
languages_other_url["markdown"]="https://github.com/ikatyang/tree-sitter-markdown"
languages_other_url["r"]="https://github.com/r-lib/tree-sitter-r"
languages_other_url["typescript"]="https://github.com/tree-sitter/tree-sitter-typescript"
languages_other_url["tsx"]="https://github.com/tree-sitter/tree-sitter-typescript"
languages_other_url["cuda"]="https://github.com/theHamsta/tree-sitter-cuda"
languages_other_url["make"]="https://github.com/alemuller/tree-sitter-make"

if [ $(uname) == "Darwin" ]
then
    soext="dylib"
else
    soext="so"
fi


# 定义一个函数,这个函数接受一个参数,此参数为所支持的语言
function get_url() {
    for i in ${languages[@]}
    do
        [ "$i" == "$1" ] && echo "https://github.com/tree-sitter/tree-sitter-$i.git"
    done
    for i in ${languages_other[@]}
    do
        [ "$i" == "$1" ] && echo "${languages_other_url[$1]}.git"
    done
}

# 构建函数
# 参数为 lang 和 url
function build_language() {
    [ -z $2 ] && echo  "此语言暂时不支持,详情输入 ./build-my.sh help 查看" && exit 0
    # 下载的脚本的地址
    module_path=`pwd`

    # Retrieve sources.
    if [ ! \( -d "tree-sitter-$1" \) ]; then
        git clone $2  --depth 1
        # 获取失败则直接返回
        [ "$?" != "0" ] && return
    fi

    # 构建地址
    # typescript 情况有点特殊
    if [ "$1" == "typescript" ]; then
        build_path="tree-sitter-typescript/typescript"
    elif [ "$1" == "tsx" ]; then
        build_path="tree-sitter-typescript/tsx"
    else
        build_path="tree-sitter-$1"
    fi

    cp tree-sitter-lang.in "$build_path/src"
    cp emacs-module.h "$build_path/src"
    cp "$build_path/grammar.js" "$build_path/src"
    cd "$build_path/src"
    # The dynamic module's c source.

    # Build.
    cc -c -I. parser.c
    # Compile scanner.c.
    if test -f scanner.c
    then
        cc -fPIC -c -I. scanner.c
    fi
    # Compile scanner.cc.
    if test -f scanner.cc
    then
        c++ -fPIC -I. -c scanner.cc
    fi

    # Link.
    if test -f scanner.cc
    then
        c++ -fPIC -shared *.o -o "libtree-sitter-$1.${soext}"
    else
        cc -fPIC -shared *.o -o "libtree-sitter-$1.${soext}"
    fi

    # Copy out.
    if [ ! \( -d "$module_path/dist" \) ]; then
        mkdir -p "$module_path/dist"
    fi

    cp "libtree-sitter-$1.${soext}" "$module_path/dist"
    cd $module_path
    rm -rf ${build_path%/*}
}

function build_help() {
    # 输出支持的语言
    echo "该脚本支持的语言如下:"
    echo ${languages[@]}
    echo ${languages_other[@]}
    echo "使用方法:
不带参数:默认构建所有脚本支持的语言
如果只有一个参数 help,打印 help,否则查看是否属于脚本支持的语言,
支持则自动开始构建,不支持提醒用户
多个参数,如果第二个参数是 url,则从该 url 获取源码构建相关语言
如果第二个参数不是 url,则认为其是所支持的语言
两个参数以上则默认所有参数都是所支持的语言
./build-my.sh 不带参数:默认构建所有脚本支持的语言
./build-my.sh help 查看支持语言以及使用方法
./build-my.sh language 对支持的语言进行构建
./build-my.sh language 'url' 对不支持的语言,通过添加 url 参数来进行构建."
    exit 0
}

if [ "$#" == 1 ]; then
    if [ "$1" == "help" ]; then
        build_help
    else
        build_language $1 `get_url $1`
    fi
elif (($#>1)); then
    if [[ "$2" == *":"* ]]; then
        build_language $1 $2
    else
        for i in $@
        do
            build_language $i `get_url $i`
        done
    fi
else
    for i in ${languages[@]} ${languages_other[@]}
    do
        build_language $i `get_url $i`
    done
fi
2 个赞

一个关于 tree-sitter 的小例子,现在我有一个很简单的小需求,是这样子的。我在编写 Emacs Lisp 或者 Common Lisp 的时候,可能需要从一些结构中跳出来并且换行。

(defun fun ()
"假设光标在这里,我想要跳出字符串并且换行"
)
;; 一般情况下我们直接 C-e C-j 就可以了

(let ((a (+ 1 2)))
  )
;;; 现在光标在 2 后面,我希望继续写下一个 bind,你可能需要 C-f C-f C-j 才行

(cond (1 (+ 1 (+ 2 3)))
      )
;;; 现在光标在 3 后面,我希望继续写下一个 clause,你可能需要 C-f C-f  C-f C-j 才行。当然了,就我上面给出的结构,你也可以直接 C-e C-j

上面的例子都是随便举的,大致明白需求就可以了,当然了,并不是每个人都有这样的需求,只是一个例子,下面是实现了:

(require 'tree-sitter)
(eval-when-compile (require 'subr-x))

(defconst flee-bind-list
  '("let" "let*" "symbol-macrolet" "symbol-macrolet*" "cl-symbol-macrolet"
    "cl-symbol-macrolet*" "if-let" "if-let*" "when-let" "when-let*")
  "保存类似 let 那样的结构,重心在 binds 的列表.")

(defconst flee-clause-list '("case" "ecase" "cl-ecase" "cl-case" "cond"
			     "eval-when" "cl-eval-when")
  "保存类似 cond 那样的结构,重心在 clause 的列表.")

(defconst flee-def-list '("defun" "cl-defun" "defmacro" "cl-defmacro")
  "保存类似定义宏,定义函数那样的结构.")

(defconst flee-target-list  (append flee-bind-list flee-clause-list flee-def-list)
  "保存想要查找的目标的列表.")

(defun flee-get-target ()
  "从当前节点开始往外查找,直到找到指定的符号."
  (tree-sitter-parent-until
   (tree-sitter-node-at (point))
   (lambda (parent)
     (member (tree-sitter-node-text (tree-sitter-node-child parent 1) t)
	     flee-target-list))))

(defun flee-child-end (parent)
  "去 `parent' 在 (point) 处的子节点的末尾."
  (goto-char
   (tree-sitter-node-end
    (tree-sitter-node-first-child-for-pos parent (point)))))

;;;###autoload
(defun flee-dwim (&optional update)
  "退出 case, ecase… 的单个 (KEYLIST BODY...) 部分,
let, let*, symbol-macrolet… 的单个 bind 部分."
  (interactive "P")
  (when update
    (setq-local tree-sitter-parser-list nil))
  (tree-sitter-get-parser-create 'tree-sitter-elisp)
  (if-let ((target (flee-get-target))
	   (sibling1 (tree-sitter-node-text
		      (tree-sitter-node-child target 1) t)))
      (if (and (member sibling1 flee-bind-list)
	       (< (point) (tree-sitter-node-end
			   (tree-sitter-node-child target 2))))
	  (flee-child-end (tree-sitter-node-child target 2))
	(if (or (member sibling1 flee-clause-list)
		(and (member sibling1 flee-def-list)
		     (tree-sitter-node-eq (tree-sitter-node-parent
					   (tree-sitter-node-at (point)))
					  target)))
	    (flee-child-end target)
	  (search-forward ")" (point-at-eol) t 1)))
    (search-forward ")" (point-at-eol) t 1))
  (newline-and-indent))


(provide 'flee)
;;; flee.el ends here

然后,你可以绑定一个快捷键比如下面这样

(define-key paredit-mode-map (kbd "C-M-j") #'flee-dwim)

当然了,如果安装了 paredit 的话,上面 (newline-and-indent) 或许可以换成 (paredit-newline)

代码整体比较简单,基本上就是对 tree-sitter 的 api 的调用,三个 list 分别代表着我上面的三种需求,你可能需要从一个结构的子结构里跳出来,也可能需要从一个结构的子结构的子结构里跳出来,默认行为为跳出右边的括号,当然,你可以根据自己的需求进行更改。

哦,对了,说下flee-dwim里的

(setq-local tree-sitter-parser-list nil)
(tree-sitter-get-parser-create 'tree-sitter-elisp)

可以通过对 flee-dwim 传入一个参数来重新创建 parser,之所以写成这样,是因为 tree-sitter 用一段时间后,偶尔会出现解析错乱的问题,我目前还没有找到稳定复现的步骤,找到的话会去提 issue 的,大家用的话如果可以稳定复现的话也可以去提个 issue,目前除了这个问题外,还没有发现其他 bug。

实际上好像也并不是所有的子结构的子结构都能跳出来,必须是类似 let 的那样的结构,实际上,你可以自己改写下面的代码

(tree-sitter-node-child target 2)

改成

(tree-sitter-node-first-child-for-pos target (point))

应该就可以了。

PS:对了,这个功能一定要先添加 tree-sitter 对 elisp 的支持,即 tree-sitter-elisp

5 个赞

关于 tree-sitter 集成到 Emacs 核心的事情,目前有什么新进展吗?

只知道作者前几天在邮件列表发了一封邮件,其他就不太清楚了:

https://lists.gnu.org/archive/html/emacs-devel/2022-03/msg00314.html

1 个赞

:+1: 看来快了。

Tree-sitter 已经加入 feature/tree-sitter 分支,喜欢尝鲜的可以去编译分支进行体验了。

相关的讨论和安装指导,可以参考邮件列表:Tree-sitter integration on feature/tree-sitter

Github 地址:GitHub - emacs-mirror/emacs at feature/tree-sitter

官网地址:savannah/emacs/feature/tree-sitter

10 个赞

很期待啊,是不是以后就不需要这个了?Emacs Tree-sitter · GitHub

1 个赞