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.
doubleclick to select term, the way nature intended pic.twitter.com/O123K12AHp
— andrew blinn (@disconcision) October 31, 2022
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 🎉