compat(indent): update EditorConfig indent guess function

The new API is actually a lot nicer for us, as we can offload more of
the work onto EditorConfig and cover a wider range of cases.
This commit is contained in:
Kiana Sheibani 2026-02-14 18:30:08 -05:00
parent 8dfe7d0ca9
commit f46b31c129
Signed by: toki
GPG key ID: 6CB106C25E86A9F7

View file

@ -2045,30 +2045,32 @@ The =indent-bars= package (provided by the above Doom module) provides some nice
*** Offset Detection *** Offset Detection
Unfortunately for packages like these, indentation in Emacs is a notoriously awful pain point. Every major mode uses a different variable to configure the number of spaces in its indents (typically referred to in Emacs lingo as the "offset"), which makes it a massive pain if you want to, say, set every language to use 2-space indents. You can't even really /read/; =indent-bars= has a hacky system to guess what offset variable to pull from, but it fails in many cases. Unfortunately for packages like these, indentation in Emacs is a notoriously awful pain point. Every major mode uses a different variable to configure the number of spaces in its indents (typically referred to in Emacs lingo as the "offset"), which makes it a massive pain if you want to, say, set every language to use a certain indent size. You can't even really /read/ the value; =indent-bars= has a hacky system to guess what offset variable to pull from, but it fails in many cases.
There isn't really a fix for this, but there is a better, more stable hack. The built-in and officially supported =editorconfig= package, which also requires manipulating indentation, maintains a variable ~editorconfig-indentation-alist~ which maps major modes to their offset variables. This won't work for every mode listed there, since =editorconfig= only has to set an offset value where we have to read one, but we can use it for the majority of cases and fall back to the default behavior if it doesn't work. There isn't really a fix for this, but there is a better, more stable hack. The built-in and officially supported =editorconfig= package, which also requires manipulating indentation, has a system for obtaining the offset variables for each major mode, via the function ~editorconfig--get-indentation~ and the variable ~editorconfig-indentation-alist~. This system lets us get the symbol for the offset variable, which can be read to find the indentation.
Problem solved, right? Not quite. We can't just read the variable it returns, because sometimes offset values in these variables need to be modified from the actual column offset, such as to be halved or doubled. We can account for this by using the most ridiculous hack I've ever devised: by telling =editorconfig= to set the indentation to 720720 spaces (a [[https://en.wikipedia.org/wiki/Superior_highly_composite_number][superior highly composite number]]) and analyzing the variables it modifies, we can reverse engineer the actual offset. For example, if it sets a certain variable to 360360, we then know that twice that variable's original value was the correct offset. We use a highly composite number since the offset must be an integer, and we must be able to recover the original value from a division operation without rounding error.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun ~/guess-offset-from-editorconfig () (defun ~/guess-offset-from-editorconfig ()
"Guess the indent offset of the current buffer based on "Guess the indent offset of the current buffer based on
`editorconfig-indentation-alist'." `editorconfig--get-indentation'."
(require 'editorconfig) (require 'editorconfig)
(let ((parent major-mode) (require 'dash)
entry) (let* ((indent-value 720720)
;; Find the closet parent mode of `major-mode' in (alist (editorconfig--get-indentation nil (number-to-string indent-value))))
;; `editorconfig-indentation-alist'. (--some (pcase it
(while (and (not (setq entry (alist-get parent editorconfig-indentation-alist))) ;; Non-mode-specific variables - ignore
(setq parent (get parent 'derived-mode-parent)))) (`(tab-width . ,_) nil)
(when (consp entry) (`(evil-shift-width . ,_) nil)
(when-let ((elem (cl-find-if (lambda (elem) (`(,var . ,val)
(or (symbolp elem) (cond ((zerop val) nil)
(and (consp elem) ((= val indent-value) (symbol-value var))
(symbolp (car elem)) ((zerop (mod indent-value val))
(integerp (cdr elem))))) (* (symbol-value var) (/ indent-value val)))
entry))) ((zerop (mod val indent-value))
(cond ((symbolp elem) (symbol-value elem)) (/ (symbol-value var) (/ val indent-value))))))
((consp elem) (/ (symbol-value (car elem)) (cdr elem)))))))) alist)))
(advice-add #'indent-bars--guess-spacing (advice-add #'indent-bars--guess-spacing
:before-until #'~/guess-offset-from-editorconfig) :before-until #'~/guess-offset-from-editorconfig)
@ -2090,7 +2092,8 @@ As a special case for exclusively this config file, we override the offset to ma
The =indent-bars= package works by overriding the face used for displaying the whitespace characters before each line. This works in most cases, but sometimes (like in Org files) the major mode uses a face to change the background color. When the bars override the face, the default background color peeks through them. We can fix this by patching one of the display functions to add the face on top rather than overriding. The =indent-bars= package works by overriding the face used for displaying the whitespace characters before each line. This works in most cases, but sometimes (like in Org files) the major mode uses a face to change the background color. When the bars override the face, the default background color peeks through them. We can fix this by patching one of the display functions to add the face on top rather than overriding.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(deftemplate! indent-bars (defun indent-bars--draw-line) (deftemplate! indent-bars
(defun indent-bars--draw-line)
(let* ... (let* ...
(when (<= bar nbars) (when (<= bar nbars)
(el-patch-remove (el-patch-remove