Subject: bug#68493: Got to be a better way to customize c-ts-mode indentation

From: Daniel Colascione <[email-masked]>
Date: Mon, 15 Jan 2024 18:51:25 -0500

There's got to be a way to customize tree-sitter-based indentation
that's cleaner than the patching I'm doing below.

In particular, we can't just prepend custom rules to those of a base
style because doing so puts those rules ahead of high-priority rules in
the base style that might account for weird syntactic conditions.

In addition, this patching isn't exactly amenable to something checked
into .dir-locals.el or file-local variables.  It's code, not data.

I really think we need a robust, *data*-based approach to describing
indentation schemes, like cc-mode's existing system.  (Wouldn't it be
nice if we could use cc-mode indentation rules with c-ts-mode?)

Maybe we have c-ts-mode--indent-styles "compile" a passive indent schema
data structure (one suitable for dir-locals) into raw tree-sitter
indentation rules?

----------

;; Why edit the K&R rules in particular?  Because the K&R rule-set is
;; the unmolested "common" rule-set.  We want to edit the rules
;; starting from this base instead of from the other styles, which
;; prefix various things to the base rules.

(defun dancol-edit-ts-rules (orig-rules patches)
  "Edit tree-sitter indentation rules

ORIG-RULES is a tree sitter rule set as described in the ELisp
manual.  PATCHES is a sequence of PATCH lists, in which each
PATCH is (:from OLD-RULE :to NEW-RULE).  OLD-RULE is a rule in
ORIG-RULES and NEW-RULE is its replacement.  This function,
unlike just prepending rules to ORIG-RULES, preserves the
ordering of rules and minimizes the degree to which style
customization might break order-dependent matching heuristics in
ORIG-RULES.

Each PATCH is PATCHES must apply at least once or this function
will signal an error.  This way, we don't allow indentation rules
to silently bitrot as the rule set we're patches evolves.

Return the edited rule list."
  (let ((rules (cl-copy-list orig-rules)))
    (pcase-dolist (`(:from ,from :to ,to) patches)
      (unless (member from orig-rules)
        (error "rule %S not in ORIG-RULES" from))
      (setf rules
            (cl-nsubstitute to from rules :test #'equal)))
    rules))

(defun dancol-edit-k&r-rules (rules)
  (dancol-edit-ts-rules rules
    `(;; Namespaces shouldn't add indentation
      (:from
       ((parent-is "declaration_list")
        parent-bol c-ts-mode-indent-offset)
       :to
       ((parent-is "declaration_list") parent-bol 0))
      ;; Don't line up operands --- just indent on continuation: nice
      ;; and simple
      (:from
       ((parent-is "binary_expression") parent 0)
       :to
       ((parent-is "binary_expression")
        parent-bol ,(* c-ts-mode-indent-offset 2)))
      ;; Indent other expression continuations instead of lineup
      ,@(mapcar (lambda (node-name)
                  `(:from
                    ((parent-is ,node-name) first-sibling 1)
                    :to
                    ((parent-is ,node-name)
                     parent-bol ,(* c-ts-mode-indent-offset 2))))
                '("parameter_list"
                  "argument_list"
                  "parenthesized_expression"))
      ,@(mapcar (lambda (node-name)
                  `(:from
                    ((parent-is ,node-name) first-sibling 0)
                    :to
                    ((parent-is ,node-name)
                     parent-bol ,(* c-ts-mode-indent-offset 2))))
                '("concatenated_string"
                  "conditional_expression"
                  "comma_expression")))))

(defun dancol-c-ts-mode--indent-styles (base-styles)
  `((dancol ,@(dancol-edit-k&r-rules
               (cdr (or (assoc 'k&r base-styles)
                        (error "no K&R style")))))
    ,@base-styles))

(advice-add 'c-ts-mode--indent-styles
            :filter-return
            #'dancol-c-ts-mode--indent-styles)

(defun dancol-c-ts-mode-setup ()
  (c-ts-mode-set-style 'dancol))

(add-hook 'c-ts-base-mode-hook #'dancol-c-ts-mode-setup)