Select term at point using tree sitter in emacs

Some days ago I saw a tweet(detail below) that show an interesting feature, a way to select terms using the mouse. After seeing that I thought it will be easy to implement using tree-sitter.

To be able to implement this we just need tree-sitter.el which will allow us to select terms using grammars.

First we need to load some libraries, including tree-sitter.el

(require 'tree-sitter)
(require 'seq)
(require 'cl-lib)

We're using overlays to highlight the term so we need a face with all the required properties.

(defvar highlight-face '((t :foreground "#000"
                            :background "#00bfff"
                            :weight bold)))

To be able to select term nodes we need to use a grammar, for this case we're going to use haskell, which has a grammar included in tree-sitter-langs.el, in modes-mapping we can define all the valid node types for each language, for haskell we just define a few of them:

  • exp_apply
  • exp_infix
  • exp_in
  • exp_cond
  • exp_literal
  • function

The order of the terms should be from small to large, for example a function which can include other terms should be defined at last otherwise the whole function will be highlighted when any part of the function is clicked.

(defvar modes-mapping '((haskell-mode . (exp_apply exp_infix exp_in exp_cond exp_literal function ))))

Now we need to define a function that will check if an term exists at point, this will be done by extracting all the predefined grammar elements for the current major-mode and check if any of them match.

(defun get-text-node-at-point ()
  "Get text node at point using predefined major mode options."
  (let ((types (alist-get major-mode modes-mapping)))
    (seq-some (lambda (type) (tree-sitter-node-at-pos type (point) t)) types)))

Now we need a function to highlight the term node at point and apply a new overlay using the face highlight-face defined lines above. when-let* is used to avoid raising an error in case there is no node at point.

(defun highlight-node-at-point ()
  "Highlight term at point."
  (interactive)
  ;; remove all previous applied overlays
  (remove-overlays (point-min) (point-max) 'face highlight-face)
  (when-let* ((node (get-text-node-at-point))
              (overlay (make-overlay (tsc-node-start-position node) (tsc-node-end-position node))))
    (overlay-put overlay 'face highlight-face)))

The last part is to call highlight-node-at-point when we click in some part of the buffer. To do this we define a function that receive a mouse event and then bind it to left mouse button.

(defun mouse-click-handler (event)
  "Run `highlight-node-at-point' using information of EVENT."
  (interactive "e")
  (save-excursion
    (goto-char (posn-point (event-start event)))
    (highlight-node-at-point)))

(global-set-key [mouse-1] #'mouse-click-handler)

This can be extended by adding more languages and node types to modes-mapping.

Enjoy 🎉