May 18, 2022 . 3 min

Playing around with tree-sitter in Emacs

Tree sitter has been one of those tools that has enabled me to add in a lot of useful features to my dev environment. I have previously written about my Emacs package meain/evil-textobj-tree-sitter which will let you operate on language constructs like functions, classes, loops etc as evil textobjects. I have been playing around with tree-sitter a lot more since and in this blog I wanted to share some interesting thing that I have been using tree-sitter for.

Navigation between textobjects #

Before we go into my custom configs, here is a new feature that got added into the package. As of now you can navigate to the next textobject of a different types. For example, you can navigate to the next function or loop. Here is my Emacs config on top of the package functionality to go to the next textobject. I have found this to be really useful to go through all the functions in the file. It comes in really handy in case of test files where go to next function is kind a like go to next test case. That combined with "run current test" is really powerful. Just in case anyone is interested, meain/toffee is what I am using to run tests under the cursor combined with a tiny bit of Emacs config.

Show current class/function name in modeline #

We use tree-sitter to parse the current class/fucntion that you are in and display it in the modeline. I have been thinking about maybe adding it to the above mentioned package or maybe even create a separate tree-sitter-tools package, but have never got around to it. Here is the function that does the heavy lifting and here is the code that adds it to the modeline(or header line in my case).

(setq meain/tree-sitter-calss-like '((rust-mode . (impl_item))
(python-mode . (class_definition))))
(setq meain/tree-sitter-function-like '((rust-mode . (function_item))
(go-mode . (function_declaration method_declaration))
(python-mode . (function_definition))))
(defun meain/tree-sitter-thing-name (kind)
"Get name of tree-sitter THING-KIND."
(if tree-sitter-mode
(let* ((node-types-list (pcase kind
('class-like meain/tree-sitter-calss-like)
('function-like meain/tree-sitter-function-like)))
(node-types (alist-get major-mode node-types-list)))
(if node-types
(let ((node-at-point (car (remove-if (lambda (x) (eq nil x))
(seq-map (lambda (x) (tree-sitter-node-at-point x))
(if node-at-point
(let ((node-name-node-at-point (tsc-get-child-by-field node-at-point ':name)))
(if node-name-node-at-point
(tsc-node-text node-name-node-at-point)))))))))

It only supports go, rust and python as of now but should be easy enuogh to add more languages. Just to give you an idea of what the code does, it looks for the node of a specific the type defined by meain/tree-sitter-calss-like or meain/tree-sitter-function-like at the current cursor position. From this node, we extract the field named name which would give us the name of the class or function. Just a heads up for people planning to use it in the modeline. Although tree-sitter is pretty fast, you might want to avoid updating this continuously in the modeline. That is why I make use of mode-line-idle so that the computation is deferred to when the cursor is idle.

Narrow to language level constructs #

I don't use this a lot, but I am glad when I have to use it that I have it. It is useful when you are concentrating on a single function or loop. It also comes in handy when showing/explaining code to someone over a call. I would usually be talking in a function context and being able to easily narrow to that function is really useful.

Just FYI, I don't use the builtin narrow, but use fancy-narrow instead. The package is not actively maintained anymore, but it works pretty well. For this, we make use of a util function from meain/evil-textobj-tree-sitter package to get the range of the textobject, and is passed over to fancy-narrow to narrow to it. You can find the code for it below.

;; Fancy narrow to textobj
(use-package emacs
:commands (meain/fancy-narrow-to-thing)
(defun meain/fancy-narrow-to-thing (thing)
(if (buffer-narrowed-p) (fancy-widen))
(let ((range (evil-textobj-tree-sitter--range 1 (list (intern thing)))))
(fancy-narrow-to-region (car range) (cdr range))))
(evil-leader/set-key "n n" (lambda () (interactive) (fancy-widen)))
(evil-leader/set-key "n f" (lambda () (interactive) (meain/fancy-narrow-to-thing "function.outer")))
(evil-leader/set-key "n c" (lambda () (interactive) (meain/fancy-narrow-to-thing "class.outer")))
(evil-leader/set-key "n C" (lambda () (interactive) (meain/fancy-narrow-to-thing "comment.outer")))
(evil-leader/set-key "n o" (lambda () (interactive) (meain/fancy-narrow-to-thing "loop.outer")))
(evil-leader/set-key "n i" (lambda () (interactive) (meain/fancy-narrow-to-thing "conditional.outer")))
(evil-leader/set-key "n a" (lambda () (interactive) (meain/fancy-narrow-to-thing "parameter.outer"))))

And that's it for now.

← Home