Run ispell on text nodes using tree sitter
Tags: emacs, treesitter
Published: 2024-06-07 02:27:18

---
title: Run ispell on text nodes using tree sitter
published: true
date: 2022-06-26 19:28:34 UTC
tags: emacs,treesitter
canonical_url: https://erick.navarro.io/blog/run-ispell-on-text-nodes-using-tree-sitter/
---

[tree-sitter](https://tree-sitter.github.io/tree-sitter/) is a great tool to have an incremental syntax tree of our code, in emacs it can be used to add syntax highlighting instead of using the regular regex based highlighting system. There are other use cases for this and now we're going to use it to build a simple tool to run [ispell](https://www.gnu.org/software/ispell/) using the content of a text node, of course we can select the text manually but it will be easier and fancy to do it in a programmatic way using the syntax tree generated by `tree-sitter`.

Emacs doesn't have support by default for `tree-sitter` so we need to install it, the following code will do it using `use-package`:

```emacs-lisp
(use-package tree-sitter
  :ensure t
  :hook
  ;; enable highlight using tree-sitter instead of regex based system
  (tree-sitter-after-on . tree-sitter-hl-mode)
  :config
  (global-tree-sitter-mode))

(use-package tree-sitter-langs
  :ensure t)
```

To build this tool we need two things:

-   Find a way to check if given our current position we are in a `string node`, it can be a literal string, a multiple line string, a comment, etc.
-   Call `ispell` pragmatically using the position of a `tree-sitter` node.

# Using tree-sitter to get node at the current position

[tree-sitter](https://github.com/emacs-tree-sitter/elisp-tree-sitter) package has some functions we can use, `tree-sitter-node-at-pos` will give us the nearest node of a given type for our current position, so if we run `(tree-sitter-node-at-post 'string (point))` if will return, in case it exists, a string node otherwise it will return `nil`, using this we can check for any possible "string" values, in a programming language we can have string, comment and other elements which have text that need a spell check.

Because `tree-sitter` use a specific grammar for every programming language, a "string" element can have different names, for example in `python` we have `string` but in `go` we have `interpreted_string_literal`, we can check this values by running `M-x tree-sitter-debug-mode` from a buffer using the language we want to know their "text" element names.

Now we have to define a list with all the supported languages we want to use:

```emacs-lisp
(setq tree-sitter-text-grammar-mapping '((python-mode . (string comment))
                                         (go-mode . (interpreted_string_literal comment))
                                         (js-mode . (string template_string comment))
                                         (elixir-mode . (string comment))))
```

Here we only support 4 languages, but it easy to add more, these are the ones I use more often.

Now with the following function we can use the previous defined list of languages and extract a valid text node at current position:

```emacs-lisp
(defun get-text-node-at-point ()
  "Get valid node for the current major mode using `tree-sitter-text-grammar-mapping'"
  (let* ((types (alist-get major-mode tree-sitter-text-grammar-mapping))
         ;; get string nodes from all the available nodes at the current point
         (matches (seq-map (lambda (x) (tree-sitter-node-at-pos x (point) t)) types))
         (filtered-matches (remove-if (lambda (x) (eq nil x)) matches)))
    ;; get first valid match
    (if filtered-matches
        (car filtered-matches))))
```


# Call ispell using a tree-sitter text node

We can use `ispell-region` to run `ispell` over a specific region, this function receives the start and end positions of a region so we need to extract those values from our `tree-sitter` text node.

`tsc-node-start-position` and `tsc-node-end-position` can be used for this:

```emacs-lisp
(defun run-ispell-on-node (node)
  "Run ispell over the text of the received `node'"
  (ispell-region (tsc-node-start-position node) (tsc-node-end-position node)))
```

# Putting all together

Now we can combine these two functions and assign it to a keybinding:

```emacs-lisp
(defun run-ispell-at-point ()
  "Run ispell at current point if there is a text node."
  (interactive)
  (let ((node (get-text-node-at-point)))
    (if node
        (run-ispell-on-node node))))

(global-set-key (kbd "C-x C-s") 'run-ispell-at-point)
```

Now when we run `C-x C-s`, if we are at a text node, `ispell` will run and check the spelling of that node.

Enjoy.