#+title: lisp-ts-mode
#+options: toc:nil

A tree-sitter alternative to ~lisp-mode~ which uses [[https://codeberg.org/zshaftel/tree-sitter-cl-syntax][this grammar]].

#+toc: headlines 3
* Why?
The killer feature of this mode is the ~FORMAT~ string support. Using a
specialized embedded grammar for format strings enables format directive
font-lock and indentation. Trust me, the font-lock helps *a lot* when writing
intricate format strings. The indentation is more niche and can be customized or
simply disabled, see [[#format_indentation]].

A sample, showing off both indentation and font-lock:

#+caption: I actually wrote that before I made this package 😂
[[file:format-screenshot.png]]

If this makes your eyes bleed, you can customize all those faces to look as
boring as you'd like.

The highlighted ~~​{​~​}~ directive after the cursor is from ~show-paren-mode~,
because ~lisp-ts-mode~'s ~syntax-propertize-function~ adds delimiter syntax to
format directives. This also means ~forward-sexp~ moves across those directives
like it does on regular lists.

Aside from that, the truth is tree-sitter doesn't add nearly as much to most
Lisp languages *on its own* compared to other languages. Emacs' syntax system is
(unsurprisingly) extremely well suited to handling Lisp syntax (notably, it even
has first class support for nested comments unlike tree-sitter). *But* the CST
generated by tree-sitter enables some /very/ fancy semantic font-lock, see the
sister package [[https://codeberg.org/zshaftel/gaudy-cl][gaudy-cl]].

* Getting started
This package is available on GNU ELPA, so here are some examples of how to
activate:
#+begin_src elisp :tangle yes :lexical yes
  (use-package lisp-ts-mode
    :ensure t)
#+end_src
If you use [[https://github.com/progfolio/elpaca][elpaca]], you can use the following recipe:
#+begin_src elisp :tangle yes :lexical yes
  (elpaca lisp-ts-mode)
#+end_src
For [[https://github.com/radian-software/straight.el][straight.el]]:
#+begin_src elisp :tangle yes :lexical yes
  (straight-use-package 'lisp-ts-mode)
#+end_src
To automatically activate ~lisp-ts-mode~ wherever ~lisp-mode~ normally activates, use
#+begin_src elisp :tangle yes :lexical yes
  (setf (alist-get 'lisp-mode major-mode-remap-alist) 'lisp-ts-mode)
#+end_src
You can add this to the ~:init~ section of ~use-package~.

I also recommend this snippet:
#+begin_src elisp :tangle yes :lexical yes
  (setf (alist-get 'lisp-ts-mode font-lock-ignore)
        lisp-ts-mode-font-lock-ignore-keywords)
#+end_src
You can place this in the ~:config~ section of ~use-package~. This disables
font-lock keywords which are already handled by the tree-sitter based font-lock
rules.

Lastly, the ~FORMAT~ string support is a separate minor mode,
~lisp-ts-format-support-mode~. You can enable it with
#+begin_src elisp :tangle yes :lexical yes
  (add-hook 'lisp-ts-mode-hook #'lisp-ts-format-support-mode)
#+end_src
But if you use [[https://codeberg.org/zshaftel/gaudy-cl][gaudy-cl]], it will apply the grammar on its own (and much more).
* ~FORMAT~ directive indentation
:PROPERTIES:
:CUSTOM_ID: format_indentation
:END:
There is optional (but currently enabled by default) auto-indentation of format
directives. Still somewhat experimental. This is mainly to indent relative to
the paired directives (~~[~, ~~{~​, ~~(~ and ~~<~). Example:
#+begin_src lisp :tangle yes
  "~<
  ~A
   ~:>"
#+end_src
After pressing ~TAB~ on the second line:
#+begin_src lisp :tangle yes
  "~<~@
     ~A
   ~:>"
#+end_src
The directive is indented to the column after the ~~<~, and the newline is
converted to a ~~@<newline>~ directive so that the indentation of the output
isn't affected. The behavior depends on a few customizable variables:
- ~lisp-ts-mode-format-indent-predicate~ :: A predicate for ~treesit-node-match-p~
  (usually a function or regexp matching a treesit node's type) to determine
  which nodes should have their /contents/ indented. By default it matches the
  four paired directives, but you can also set it to indent relative to the
  start of the entire format string. *To disable format indentation entirely,
  set it to nil.*
- ~lisp-ts-mode-format-indent-auto-escape-eol~ :: Controls the behavior of the
  newline directive. It can be set to nil (don't indent if there isn't already a
  ~~<newline>~), t (don't add it but indent anyway), a string specifying the
  directive prefix to add, or a cons pair ~(LOGICAL-BLOCK . DEFAULT)~ to specify
  a different string (like ~~:@_~~) to use within ~~<​~:>~.
- ~lisp-ts-mode-format-indent-tilde-relative~ ::
  # FIXME wow this is horrible wording
  Determines which column is used as an anchor for calculating indentation
  (using the following two variables), and the indentation of the end of a
  paired directive when it begins a line. If nil (the default), the directive
  characters themselves are aligned, like
  #+begin_src lisp :tangle yes
    "~:@{
         ~A
       ~}"
  #+end_src
  If set to non-nil, the ~s are aligned:
  #+begin_src lisp :tangle yes
    "~:@{
      ~A
     ~}"
  #+end_src
- ~lisp-ts-mode-format-group-indent-offset~ :: Number of columns added to
  indentation relative to the start of a paired directive.
- ~lisp-ts-mode-format-string-indent-offset~ :: Like the above but controls
  indentation relative to the start of the string, if that's enabled by
  ~lisp-ts-mode-format-indent-predicate~.
- ~lisp-ts-mode-format-indent-function~ :: This variable isn't customizable
  because it's meant to be used with ~add-function~. This is the function called
  to perform the indentation, so you can wrap it to control if, when or how the
  indentation is performed.

For more details see each variable's documentation.
* Font lock
There are distinct faces for every component of format directives I could think
of: ~~~, numeric, character, ~V~ and ~#~ parameters, the ~,~ separator between
parameters, ~:~ and ~@~ modifiers and the directive character itself all have
specific faces. The paired directives (~~[~, ~~{~​, ~~(~ and ~~<~) have a
separate face from other directives, and you can customize
~lisp-ts-mode-format-rainbow-delimiters~ to use [[https://github.com/Fanael/rainbow-delimiters][rainbow-delimiters]] faces to
highlight those directives based on nesting level. There are also three faces
for each level of ~#||#~ comment nesting (the top level uses
~font-lock-comment-face~). See ~M-x customize-group RET lisp-ts-mode~.
