diff --git a/config.org b/config.org index c1be4ef..2d2e045 100644 --- a/config.org +++ b/config.org @@ -930,7 +930,6 @@ workspaces deft ;;(emoji +unicode) hl-todo -indent-guides ;;ligatures ;;minimap (popup +defaults) @@ -1977,6 +1976,94 @@ Forge is a convenient package for working with remote code forges like GitHub, G forge-alist)) #+end_src +** Indentation + +#+call: confpkg("Pkg: indent-bars") + +#+begin_src emacs-lisp :noweb-ref doom-module +:ui indent-guides +#+end_src + +The =indent-bars= package (provided by the above Doom module) provides some nice vertical lines to mark indentation. + +*** 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. + +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. + +#+begin_src emacs-lisp +(defun ~/guess-offset-from-editorconfig () + "Guess the indent offset of the current buffer based on +`editorconfig-indentation-alist'." + (require 'editorconfig) + (let ((parent major-mode) + entry) + ;; Find the closet parent mode of `major-mode' in + ;; `editorconfig-indentation-alist'. + (while (and (not (setq entry (alist-get parent editorconfig-indentation-alist))) + (setq parent (get parent 'derived-mode-parent)))) + (when (consp entry) + (when-let ((elem (cl-find-if (lambda (elem) + (or (symbolp elem) + (and (consp elem) + (symbolp (car elem)) + (integerp (cdr elem))))) + entry))) + (cond ((symbolp elem) (symbol-value elem)) + ((consp elem) (/ (symbol-value (car elem)) (cdr elem)))))))) + +(advice-add #'indent-bars--guess-spacing + :before-until #'~/guess-offset-from-editorconfig) +#+end_src + +As a special case for exclusively this config file, we override the offset to match Emacs Lisp. This is because Org mode doesn't really need indentation under normal circumstances, but the Emacs Lisp code embedded in this literate program does. + +#+begin_src emacs-lisp +(add-hook! org-mode + (defun ~/org-mode-config-indent-bars () + (when (equal (buffer-file-name) + (expand-file-name "config.org" doom-user-dir)) + (setq indent-bars-spacing-override lisp-body-indent) + (indent-bars-mode +1)))) +#+end_src + +*** Appearance + +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 +(deftemplate! indent-bars (defun indent-bars--draw-line) + (let* ... + (when (<= bar nbars) + (el-patch-remove + (if indent-bars--no-stipple + (setq prop 'indent-bars-display fun #'indent-bars--no-stipple-char) + (setq prop 'face fun #'indent-bars--face))) + (let ((pos (if tabs start (+ start indent-bars--offset)))) + (el-patch-add + (if indent-bars--no-stipple + (setq prop 'indent-bars-display fun #'indent-bars--no-stipple-char) + (setq prop 'face + fun (lambda (style depth) + (cons (indent-bars--face style depth) + (ensure-list (get-text-property pos 'face))))))) + ...)))) +#+end_src + +*** Evil Shift Width + +The Vim/Evil keybinds =<= and =>= shift the target left or right by one indent respectively, where the specific offset it uses is determined by ~evil-shift-width~. Doom Emacs sets this equal to ~tab-width~, the displayed width of a hard tab, which is a little odd, as that doesn't actually have anything to do with the indentation settings of the current mode. We can fix this in a few different ways, but the simplest is to set ~tab-width~ to the offset to make everything consistent. + +#+begin_src emacs-lisp +(add-hook! 'change-major-mode-after-body-hook + (defun ~/adjust-tab-width () + (unless (or (derived-mode-p 'org-mode) ;; Org mode is an exception + (local-variable-p 'tab-width)) + (setq-local tab-width (or (~/guess-offset-from-editorconfig) + tab-width))))) +#+end_src + ** Marginalia #+call: confpkg("Pkg: marginalia")