Emacs: Maintaining proper indentation in indentation-sensitive programming languages

As codebases grow, maintaining proper indentation becomes increasingly difficult, especially in languages like Python or YAML, where indentation is not just a matter of style but an important part of the syntax. When working with large code blocks or deeply nested structures, it’s easy to lose track of the correct indentation level, leading to errors and decreased readability. In this post, we’ll explore Emacs packages and an Elisp code snippet that can help manage indentation in these indentation-sensitive languages.

Code snippet: Indenting new lines based on previous non-blank line

The following code snippet configures Emacs to indent based on the indentation of the previous non-blank line:

;; This ensures that pressing Enter will insert a new line and indent it.
(global-set-key (kbd "RET") #'newline-and-indent)

;; Indentation based on the indentation of the previous non-blank line.
(setq-default indent-line-function #'indent-relative-first-indent-point)

;; In modes such as `text-mode', pressing Enter multiple times removes
;; the indentation. The following fixes the issue and ensures that text
;; is properly indented using `indent-relative' or
;; `indent-relative-first-indent-point'.
(setq-default indent-line-ignored-functions '())Code language: Lisp (lisp)

Emacs package: indent-bars (Indentation guides)

The indent-bars Emacs package, written by JD Smith, enhances code readability by providing visual indentation guides, optimized for speed and customization. It supports both space and tab-based indentation and offers optional tree-sitter integration, which includes features like scope focus. The appearance of the guide bars is highly customizable, allowing you to adjust their color, blending, width, position, and even apply a zigzag pattern. Depth-based coloring with a customizable cyclical palette adds clarity to nested structures. The package also features fast current-depth highlighting, configurable bar changes, and the ability to display bars on blank lines. Additionally, it maintains consistent bar depth within multi-line strings and lists, and it works seamlessly in terminal environments using a vertical bar character. (Send your customizations to JD Smith, the author of indent-bars. He mentioned on Reddit that, “If you or others have customized the bar style settings, I’d be happy to add them to the examples page”.)

The indent-bars package isn’t available in any package database yet, but you can install it using straight.

  1. If you haven’t already done so, add the straight.el bootstrap code to your init file.
  2. After that, add the following code to your Emacs init file:
(use-package indent-bars
  :ensure t
  :commands indent-bars-mode
  :straight (indent-bars
             :type git
             :host github
             :repo "jdtsmith/indent-bars")
  :hook ((yaml-mode . indent-bars-mode)
         (yaml-ts-mode . indent-bars-mode)
         (python-mode . indent-bars-mode)
         (python-ts-mode . indent-bars-mode))
  :custom
  (indent-bars-prefer-character t))Code language: Lisp (lisp)

The indent-bars fancy guide bars when indent-bars-prefer-character is set to nil:

If you are using Linux or macOS (not PGTK on Linux, NS on macOS, or Windows), try setting the indent-bars-prefer-character variable to nil to make indent-bars display fancy guide bars using the :stipple face attribute (see indent-bars compatibility). On macOS, it only works if you are using the non-NS version of Emacs, known as emacs-mac-app, which can be installed from MacPorts using the emacs-mac-app or emacs-mac-app-devel package.

You can have Emacs automatically set the indent-bars-prefer-character variable to nil when the window system is PGTK or NS, where the stipple attribute is not supported and using the character is preferred, with the following Elisp code:

;; Make the indent-bars package decide when to use the stipple attribute
(setq indent-bars-prefer-character
      (if (memq initial-window-system '(pgtk ns)) t))Code language: Lisp (lisp)

Emacs package: outline-indent.el (Code folding)

The outline-indent.el Emacs package provides a minor mode that enables code folding based on indentation levels for various indentation-based text files, such as YAML, Python, and any other indented text files.

To install the outline-indent from MELPA, add the following code to your Emacs init file:

(use-package outline-indent
  :ensure t
  :commands (outline-indent-minor-mode
             outline-indent-insert-heading)
  :hook ((yaml-mode . outline-indent-minor-mode)
         (yaml-ts-mode . outline-indent-minor-mode)
         (python-mode . outline-indent-minor-mode)
         (python-ts-mode . outline-indent-minor-mode))
  :custom
  (outline-indent-ellipsis " ▼ "))Code language: Lisp (lisp)

In addition to code folding, outline-indent allows moving indented subtrees up and down, promoting and demoting sections to adjust indentation levels, customizing the ellipsis, and inserting a new line with the same indentation level as the current line, among other features.

Emacs Package: dtrt-indent (Guessing the original indentation offset)

The dtrt-indent provides an Emacs minor mode that detects the original indentation offset used in source code files and automatically adjusts Emacs settings accordingly, making it easier to edit files created with different indentation styles.

(use-package dtrt-indent
  :ensure t
  :commands (dtrt-indent-global-mode
             dtrt-indent-mode
             dtrt-indent-adapt
             dtrt-indent-undo
             dtrt-indent-diagnosis
             dtrt-indent-highlight)
  :config
  (dtrt-indent-global-mode))Code language: Lisp (lisp)

Code snippet: Inserting a new line before the next line that has the same or less indentation level

(If you’re using outline-indent.el, there’s no need for the Elisp code below. You can simply use the (outline-indent-insert-heading) function.)

You can use the following function to inserts a new line just before the next line that has the same or less indentation level:

(defun my-insert-line-before-same-indentation ()
  "Insert a new line with the same indentation level as the current line.
The line is inserted just before the next line that shares the same or less
indentation level. This function finds the nearest non-empty line with the same
or less indentation as the current line and inserts a new line before it.

This function is part of the outline-indent (MELPA) Emacs package.
It was extracted from the function (outline-indent-insert-heading)
written by James Cherti and distributed under the GPL 3.0 or later license."
  (interactive)
  (let ((initial-indentation nil)
        (found-point nil))
    (save-excursion
      (beginning-of-visual-line)
      (setq initial-indentation (current-indentation))
      (while (and (not found-point) (not (eobp)))
        (forward-line 1)
        (if (and (>= initial-indentation (current-indentation))
                 (not (looking-at-p "^[ \t]*$")))
            (setq found-point (point))))
      (when (and (not found-point) (eobp))
        (setq found-point (point))))
    (when found-point
      (goto-char found-point)
      (forward-line -1)
      (end-of-line)
      (newline)
      (indent-to initial-indentation))))Code language: Lisp (lisp)

If you are an Emacs Evil mode user, here’s an additional function that switches to insert mode after inserting a new line with matching indentation:

(with-eval-after-load "evil"
  (defun my-evil-insert-line-before-same-indentation ()
    "Insert a new line with the same indentation level as the current line."
    (interactive)
    (my-insert-line-before-same-indentation)
    (evil-insert-state))

  ;; Pressing Ctrl-Enter calls my-evil-insert-line-before-same-indentation 
  (evil-define-key '(normal insert) 'global (kbd "C-<return>")
    #'my-evil-insert-line-before-same-indentation))Code language: Lisp (lisp)

Built-in feature: indent-rigidly

The indent-rigidly built-in Emacs feature (C-x TAB) allows for manual adjustment of indentation by shifting a block of text left or right. It makes it easy to adjust indentation levels interactively. This can be especially useful for fine-tuning indentation in code or text where automatic tools might not always get it right. (By the way, moving the entire block with indent-rigidly is similar to the promote/demote functions in the outline-indent.el package.)

Related links

  • Emacs documentation: Indentation
  • block-nav: Allows navigation through code based on indentation.
  • aggressive-indent: Automatically maintains proper indentation throughout your code. Works better with languages such as Elisp, C/C++, Javascript, CSS…
  • Combobulate: Combobulate enhances structured editing and movement for various programming languages by leveraging Emacs 29’s tree-sitter library. Combobulate uses tree-sitter’s concrete syntax tree for precise code analysis, resulting in more accurate movement and editing.
  • expand-region: Expand the selected region by semantic units by repeatedly pressing the key until the desired area is highlighted.
  • outline-indent.el alternative:
    • origami.el: No longer maintained, slow, and have known to have bugs that affect its reliability and performance.
    • yafolding.el: No longer maintained and slow. It does not work out of the box with Evil mode and evil-collection.
  • Indent-bars alternatives (they work, are no longer maintained):
    • indent-guide: An older indent-bars alternative that uses overlays with | characters. There are some performance concerns reported, and it is incompatible with company and other similar in-buffer modes. (indent-bars is better.)
    • highlight-indentation-mode: An indent-bars alternative that uses overlays to display indentation guides and includes a mode for showing the current indentation level. It offers partial support for guides on blank lines. (indent-bars is better.)
    • highlight-indent-guides: An indent-bars alternative that offers a highly customizable indentation highlighting, featuring options for color, style, and current depth indication. (indent-bars is better.)
    • hl-indent-scope: An indent-bars alternative that highlights indentation based on language scope, requiring specific support for each language, and uses overlays to display indentation guides. (indent-bars is better.)
    • visual-indentation-mode: An indent-bars alternative that uses full character-based alternating color indentation guides. The package is now archived. (indent-bars is better.)

Conclusion

This article has highlighted various Emacs packages and Elisp code snippets to enhance indentation management in indentation sensitive programming languages.

It took me a while to find the packages mentioned in this article, as I had to test many of them. Unfortunately, many popular packages are unmaintained, slow, or have unresolved bugs. I’ve only shared the packages that work flawlessly. For instance, while the Origami package is widely used, it’s slow, buggy, and no longer maintained. The outline-indent.el package is a more modern alternative for folding indented text, aligning with the trend of utilizing built-in Emacs features (like Corfu, Cape, Vertico, Consult…). Similarly, indent-bars provides a more refined experience than older packages like highlight-indent-guides and highlight-indentation.

If you have any other packages or Elisp code you rely on for managing indentation in sensitive languages, I’d love to hear about them.