Skip to content

Instantly share code, notes, and snippets.

@sogaiu
Last active May 13, 2026 07:33
Show Gist options
  • Select an option

  • Save sogaiu/1f640325c50c609c2231534b30bbc069 to your computer and use it in GitHub Desktop.

Select an option

Save sogaiu/1f640325c50c609c2231534b30bbc069 to your computer and use it in GitHub Desktop.
janet-ts-mode custom indentation

The latest approach is to try to get different behavior based on whether the indentation is line-related or region-related.

Emacs provides two variables that can be set to functions that handle each type of indentation:

  • indent-region-function, and
  • indent-line-function

The idea is to provide an appropriate function for each of these variables.

I've tried out the code below along with the following commands:

  • indent-for-tab-command (Tab)
  • indent-region,
  • prog-indent-sexp (C-M-q),
  • prog-fill-reindent-defun (M-q)

and so far things seem to be mostly working for my usage.

If you have any other indentation-related functions that you use, please mention them.


If you wish to try things out, here are some instructions for use:

  1. Save the code at the end of this comment in a file named janet-ts-custom-indent.el and make sure it is on the load-path for your Emacs setup (or change your Emacs settings so that require will work for it).
  2. Add the following (or similar) to your .emacs-equivalent file:
(require 'janet-ts-custom-indent)

;; choose ONE of the following:

;; 1. indent to align with opening delimiter
'(setq janet-ts-ml-long-string-anchor-fn
      #'janet-ts-align-to-opening-delim-anchor
      janet-ts-ml-long-string-offset-fn
      #'janet-ts-align-to-opening-delim-offset)

;; 2. indent relative to "most recent" non-blank line
(setq janet-ts-ml-long-string-anchor-fn
      #'janet-ts-relative-to-previous-anchor
      janet-ts-ml-long-string-offset-fn
      #'janet-ts-relative-to-previous-offset)

;; arrange to work when janet-ts-mode is enabled
(add-hook 'janet-ts-mode-hook
          #'janet-ts-custom-ml-long-string-indent)

Here is the content of janet-ts-custom-indent.el:

;;; janet-ts-custom-indent --- Custom indentation examples

;;; Commentary:

;; Example custom indentation code.  Experimental.  May change.

;; The basic idea is to line-indent differently within multi-line
;; docstrings than the default behavior of not doing anything.
;;
;; Two examples are provided.
;;
;; 1) Indents to match the opening delimiter of the long-string.
;;
;; 2) Indents to match the first non-blank line searching backwards.
;;    This allows a little extra control over indentation.
;;
;; To use this file, ensure it's on `load-path' and include an
;; appropriately modified version of the following code in your
;; `.emacs'-equivalent file of choice:
;;
;;   ;; make sure the code in this file can be used
;;   (require 'janet-ts-custom-indent)
;;
;;   ;; choose ONE of the following:
;;
;;   ;; 1. indent to align with opening delimiter
;;   (setq janet-ts-ml-long-string-anchor-fn
;;         #'janet-ts-align-to-opening-delim-anchor
;;         janet-ts-ml-long-string-offset-fn
;;         #'janet-ts-align-to-opening-delim-offset)
;;
;;   ;; 2. indent relative to "most recent" non-blank line
;;   (setq janet-ts-ml-long-string-anchor-fn
;;         #'janet-ts-relative-to-previous-anchor
;;         janet-ts-ml-long-string-offset-fn
;;         #'janet-ts-relative-to-previous-offset)
;;
;;   ;; arrange to work when janet-ts-mode is enabled
;;   (add-hook 'janet-ts-mode-hook
;;             #'janet-ts-custom-ml-long-string-indent)

;;; Code:

(require 'treesit)
(require 'janet-ts-mode) ; for janet-ts--indent-rules

(defun janet-ts--node-is-multi-line (node)
  "Check whether NODE is multi-line."
  (let* ((n-start (treesit-node-start node))
         (start-line (line-number-at-pos n-start))
         (n-end (treesit-node-end node))
         (end-line (line-number-at-pos n-end)))
    (not (= start-line end-line))))

(defun janet-ts--in-multi-line-long-string-p ()
  "Check whether point is in a multiline long-string."
  (let* ((here (point))
         ;; treesit-node-at can give misleading answer:
         ;; immediately to the left of the opening delimiter or
         ;; immediately to the right of the closing delimiter
         ;; can give result that within long-string even if not
         (here-node (treesit-node-on here here)))
    (and (string= "long_str_lit" (treesit-node-type here-node))
         (janet-ts--node-is-multi-line here-node))))

(defun janet-ts--indent-ml-long-string-align-to-opening-delim (node parent bol)
  "If within a multi-line long-string, indent to align to delimiter.

More precisely, lines that appear after the long-string's opening
delimiter (but not on the same line as the delimiter) are indented to
match the left-most character of the long-string's opening delimiter.

For info on NODE, PARENT, and BOL, see `treesit-indent-function'."
  (let* ((here (point))
         (here-node (treesit-node-on here here))
         (anchor (treesit-node-start here-node))
         (start-line (line-number-at-pos anchor))
         (current-line (line-number-at-pos here))
         (offset nil))
    (if (or (= start-line current-line)
            (= here anchor))
        ;; hand off to the default
        (cons nil nil)
      (save-excursion
        (goto-char anchor)
        ;; move to first non-whitespace character
        (back-to-indentation)
        ;; compute indentation offset
        (setq offset (- (point) anchor)))
      (cons anchor offset))))

;; XXX: this approach is duplicated effort, but it's unclear how to
;;      avoid that
(defun janet-ts-align-to-opening-delim-anchor (node parent bol)
  "Anchor wrapper function.

Wraps `janet-ts--indent-ml-long-string-align-to-opening-delim' to get
its car, which corresponds to what an anchor function would have
produced.

For info on NODE, PARENT, and BOL, see `treesit-indent-function'."
  (let ((result (janet-ts--indent-ml-long-string-align-to-opening-delim
                 node parent bol)))
    (when result
      (car result))))

(defun janet-ts-align-to-opening-delim-offset (node parent bol)
  "Offset function.

Wraps `janet-ts--indent-ml-long-string-align-to-opening-delim' to get
its cdr, which corresponds to what an offset function would have
produced.

For info on NODE, PARENT, and BOL, see `treesit-indent-function'."
  (let ((result (janet-ts--indent-ml-long-string-align-to-opening-delim
                 node parent bol)))
    (when result
      (cdr result))))

(defun janet-ts--indent-ml-long-string-relative-to-previous (node parent bol)
  "If within a multi-line long-string, indent to match previous line.

More precisely, lines that appear after the long-string's opening
delimiter (but not on the same line as the delimiter) are indented to
match the first previous non-blank line searching backwards.

For info on NODE, PARENT, and BOL, see `treesit-indent-function'."
  (let* ((start (point))
         (start-node (treesit-node-on start start))
         (close-line (line-number-at-pos (treesit-node-end start-node)))
         (anchor (treesit-node-start start-node))
         (start-line (line-number-at-pos anchor))
         (current-line (line-number-at-pos start))
         (current-line-beginning nil)
         (offset nil))
    (cond ((or (= start-line current-line)
               (= start anchor))
           ;; hand off to the default
           (cons nil nil))
          ;; arrange for closing delimiter with opening delimiter alignment
          ((= close-line current-line)
           (let ((open-delim (treesit-node-start start-node)))
             (save-excursion
               (goto-char open-delim)
               (beginning-of-line)
               (setq current-line-beginning (point))
               (back-to-indentation)
               (setq offset (- (point) current-line-beginning)))
             (cons current-line-beginning offset)))
          ;; look for relevant previous line and compute accordingly
          (:else
           (let* ((found nil)
                  (start-line-beginning nil))
             (save-excursion
               (beginning-of-line)
               (setq start-line-beginning (point))
               (goto-char start)
               (while (and (not found)
                           (not (= start-line current-line)))
                 ;; move to previous line
                 (forward-line -1)
                 ;; check if line is blank
                 (when (not (looking-at-p "[[:blank:]]*$"))
                   (setq found t)
                   (when (not (= start-line current-line))
                     (setq current-line-beginning (point))
                     ;; move to first non-whitespace character
                     (back-to-indentation)
                     ;; compute indentation offset
                     (setq offset (- (point) current-line-beginning))))))
             (if (= start-line current-line)
                 (cons nil nil)
               (cons start-line-beginning offset)))))))

;; XXX: this approach is duplicated effort, but it's unclear how to
;;      avoid that
(defun janet-ts-relative-to-previous-anchor (node parent bol)
  "Anchor wrapper function.

Wraps `janet-ts--indent-ml-long-string-relative-to-previous` to get its
car, which corresponds to what an anchor function would have produced.

For info on NODE, PARENT, and BOL, see `treesit-indent-function'."
  (let ((result (janet-ts--indent-ml-long-string-relative-to-previous
                 node parent bol)))
    (when result
      (car result))))

(defun janet-ts-relative-to-previous-offset (node parent bol)
  "Offset function.

Wraps `janet-ts--indent-ml-long-string-relative-to-previous` to get its
cdr, which corresponds to what an offset function would have produced.

For info on NODE, PARENT, and BOL, see `treesit-indent-function'."
  (let ((result (janet-ts--indent-ml-long-string-relative-to-previous
                 node parent bol)))
    (when result
      (cdr result))))

(defvar janet-ts-ml-long-string-anchor-fn
  #'janet-ts-relative-to-previous-anchor
  "Anchor function for multi-line long-string line indentation.

See `treesit-simple-indent-rules' for details.")

(defvar janet-ts-ml-long-string-offset-fn
  #'janet-ts-relative-to-previous-offset
  "Offset function for multi-line long-string line indentation.

See `treesit-simple-indent-rules' for details.")

(defun janet-ts-custom-ml-long-string-indent ()
  "Hook function for specialized multi-line long-string indentation.

This function locally sets `indent-region-function' and
`indent-line-function' to functions that provide specialized
behavior for indentation such that multi-line long-strings are
handled appropriately in various contexts.

The purpose of the region-related function is to ensure that indenting
regions that contain multi-line long-strings don't adversely
modify the content of multi-line long-strings.

The purpose of the line-related function is to make it possible to have
indentation behave differently within multi-line long-strings when
line-indentation is requested for that context."
  (setq-local
   ;; special handling for region indentation
   indent-region-function
   (lambda (beg end)
     (if (janet-ts--in-multi-line-long-string-p)
         (let ((treesit-simple-indent-rules janet-ts--indent-rules))
           (treesit-indent-region beg end))
       (treesit-indent-region beg end)))
   ;; special handling for line indentation
   indent-line-function
   (lambda ()
     (if (janet-ts--in-multi-line-long-string-p)
         (let ((treesit-simple-indent-rules
                ;; N.B. value is a slightly modified janet-ts--indent-rules
                `((janet-simple
                   ((parent-is "source")
                    parent-bol 0)
                   ;; multi-line long-string - can be top-level or not
                   ((parent-is "long_str_lit")
                    ,janet-ts-ml-long-string-anchor-fn
                    ,janet-ts-ml-long-string-offset-fn)
                   ((or (parent-is "sqr_tup_lit") (parent-is "struct_lit"))
                    parent 1)
                   ((or (parent-is "par_arr_lit") (parent-is "sqr_arr_lit")
                        (parent-is "tbl_lit"))
                    parent 2)
                   ((parent-is "par_tup_lit")
                    janet-ts--anchor-for-par-tup-parent 0)))))
           (treesit-indent))
       (treesit-indent)))))

(provide 'janet-ts-custom-indent)
;;; janet-ts-custom-indent.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment