EVIL: Extensible Jump Item :: %

This will not be a literate program.
This is just a rundown of a file I have been hacking on the past year, trying to get VIM behaviour in EMACS, such as jumping from fi to if in bash and the like.

Fair warning: this file relies on EMACS 29.1 AT LEAST behaviour.
The file being. All this file requires is a comment macro, Evil being installed, and the dependencies in the require macro. Also:

Also, it might silently rely on 30.1 behaviour I am not aware of, so.

Installing treesitter grammars

Essentially, what you need is a list of repositories to pull from, and to invoke a way of installing them, and EMACS handles the loading and everything from there on in.
All you need to do from there is then futz (RPLACD) the rules within auto-mode-alist.


(setq treesit-language-source-alist
   '(;; ones i have demonstrated the % movement
     (bash "https://github.com/tree-sitter/tree-sitter-bash")
     (html "https://github.com/tree-sitter/tree-sitter-html")
     (zsh "https://github.com/georgeharker/tree-sitter-zsh")
     (sml "https://github.com/MatthewFluet/tree-sitter-sml")
     ;;; ...
     ))
(comment
 (mapc (lambda (x)
         (unless (treesit-language-available-p x)
           (treesit-install-language-grammar x)))
       (mapcar #'car treesit-language-source-alist))
 t)

The modes that use these, can be installed with whichever package manager of your choosing, but I prefer Elpaca, primarily because it actually handles recipes specifying git tags, git branches and git commits to clone from, allowing one to lock at install time. This is in contrast to whatever straight is doing, which I couldn't figure out.

On with the show,,,

Demonstration:

The Motivation

The motivation, as touched on earlier, was to move around HTML and other buffer types via the % key. Moving around otherwise with Evil can be quite cumbersome, and at the time I had not known of sgml-skip-tag-backward . Even if I knew of that function, or others similar, I would prefer them bound to what I am used to when I use VIM or similar. Hence, I had to alter the behaviour of Evil's % with advice-add.

What is EMACS's Advice system?

In simple terms, advice lets you run code around another piece of code, ultimately replacing the entrypoint in the symbol table. You can add a piece of code that runs :before, :after, and :around.
I ultimately just use :around, because I don't know what I want, and usually want to know what the code path is. I also usually replace the code path entirely.

The following code is likely best used with a :before, but I use an :around.


;;; MANAGE MESSAGE BUFFER (defun clear-message-buffer () (interactive) (with-current-buffer (get-buffer "*Messages*") (named-let lp ((it buffer-read-only)) (when it (read-only-mode -1) (lp (not it))) (delete-region (point-min) (point-max)) (read-only-mode)))) (defvar message-size-threshold 5000) (advice-add 'message :around (lambda (fn format-string &rest args) (with-current-buffer (get-buffer "*Messages*") (let ((size (buffer-size (current-buffer)))) (when (> size message-size-threshold) (clear-message-buffer)))) (apply fn (cl-list* format-string args))) '((name . "memory-mgr"))) ;;(advice-remove 'message "memory-mgr")

The purpose of the previous code is so that I do not accumulate hours worth of messages from EMACS over a course of a hacking session. This would have been more applicable to older systems, where a few megabytes of RAM being wasted on debug info would have been costly. This is more for legibility of trying to find out what happened in the last half hour.

What would I need to do to make % extensible?

Essentially, what one needs is some way of testing for a piece of lexical syntax, and then dispatching a piece of code to move the editor's point around.
I have no idea how one would do this without a table.
If this were like, smalltalk, one would still have a table, but then you don't need funcall and rely on polymorphism instead.
le

JumpTable>>determineAndRun [
  self caseOf: table :: value: universalArgument.
].

Because of the fact that one needs to be able to add to this at runtime, you cannot forgo using a table.
I am open to suggestions, but I am doubtful of this
Hence, we come to this.


(defun evil-extensible-jump-item/advice (fn &optional count)
  "Advice for enabling the extensible jumping behaviour."
  (let (do)
    (if (cl-some (lambda (x)
                   (let ((tst (funcall (car x))))
                     (when tst
                       (setf do (cdr x))
                       tst)))
                 evil-extensible-jump-item/*table*)
        (funcall do count)
      ;; fallthrough to original function
      (apply fn count))))

(advice-add 'evil-jump-item :around
  #'evil-extensible-jump-item/advice
  '((name . "evil-extensible-jump-item")))
    

One immediate concern would be, how would I share data between functions?
How would I avoid recomputing nodes in a treesitter tree that got made for the test function?
The shrimple answer is this: Let over Lambda.
I initially only used it because of the shotgun regex parsing I had with a bash pipeline movement function, where I computed a pipeline's tree from the test function and returning true if the node-at-point was within it.
But I came to use it in every example of the SML movement functions.

So when I determined I was within a case expression, I kept the node in a unbound let slot that was shared amoung the tester and mover function. I could then determine the next MRULE node within that case, and hence move to it, without re-walking the tree to get a case node, wasting cycles[1]. WITH ENDLESS OPTIONS FOR RENEWAL.

Caveats

This overwhelmingly relies on treesitter.
This also relies on treesitter parses being similar.
Hence, if theres a difference in parsing, one function could do unexpected things when being ran along side another treesitter grammar.

Another caveat is that of load order, and test-function clobbering. z03-extjmp/sml-patfun-test, in particular, if it did not test for parent nodes not being that of CASE_EXP, and was added to the table earlier: then it would run inside the pattern of a case expression, and return to the beginning of the function or go to the other pattern entry point of the function.

This is less of a caveat and more of a deliberate decision, but you require the use of mode hooks to register functions in the table. Otherwise, EVERY function related would be in the table at the same time, bloating it and possibly doing the "clobbering" I mentioned earlier. This is a feature of EMACS called "buffer local variables".
I could provide a macro for this, similar to my add-named-hook macro, but I don't have enough of these to warrant doing so. It would just be a (progn (defun ... () ...) (add-hook ... ...)), so.