SmartTabs

Note: This is about indenting with tabs and aligning with spaces. For Sébastien Rocca-Serra’s smart-tab command, see TabCompletion.

TabsSpacesBoth
Tabs and spaces are often misunderstood. (Original comic by Steve Napierski.)

There is a semantic way of using tab characters in source code: tabs for indentation, spaces for alignment. This ensures that the code is displayed correctly everywhere, regardless of the viewer’s tab size.

    // Tab size: 4                // Tab size: 2
    if(foo) {                     if(foo) {
    --->char quux[4] = {0, 1,     ->char quux[4] = {0, 1,     // "0" and "2"
    --->................2, 3};    ->................2, 3};    // remain aligned
    }                             }

The approach can be summed up in two points:

  1. Tabs are only used at the beginning of lines. Everything else, like ASCII art and tables, should be formatted with spaces.
  2. Tabs are only used for expressing the indentation level. One tab per “block” – any remaining whitespace is spaces only.

This page provides code which improves on Emacs’ auto-indenting, so that whenever you press the <tab> key, the appropriate amount of tabs and spaces is inserted automatically. You can still use all spaces if you prefer, of course; the code merely ensures that if you enable tabs (maybe when maintaining legacy code), the tab size does not matter.

Packaging

This code is now available as an Emacs minor mode through the MELPA package smart-tabs-mode and can be installed through the Emacs package manager. You can add MELPA to your package archive list with:

    (add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/"))

The source code is available at GitHub; see the file smart-tabs-mode.el. Bug reports, code contributions, etc., are all welcome. —JCSalomon

Setup

If you installed Smart Tabs Mode through Emacs’ package manager, the only thing that you need to do is tell it which languages you wish to use it for. This can be accomplished with the smart-tabs-insinuate function. Just pass this function the symbols identifying the languages that you want it to be enabled for and the appropriate hooks will be set up automatically. For example, to enable smart-tabs-mode automatically for C and Javascript, you would add the following to your config:

    (smart-tabs-insinuate 'c 'javascript)

That’s it! To view the list of available languages, enable the minor mode with ‘M-x smart-tabs-mode’ and then run ‘M-x describe-variable RET smart-tabs-insinuate-alist RET’. You will see an alist of the languages supported as well as the hooks that will get added if you decide to turn it on.

Use

To make Emacs insert the proper amount of tabs and spaces automatically, provided indent-tabs-mode is not disabled (see toggling) just press ‘<tab>’ as usual.

You can retab the whole file by pressing ‘C-x h C-M-\’. (TabCompletion provides a way to do this with fewer keystrokes.)

Toggling

Smart tabs are only used when indent-tabs-mode is non-nil (default). Type ‘C-h v indent-tabs-mode RET’ to see the current, buffer-local value. To change indent-tabs-mode for the current buffer only, type ‘M-x set-variable indent-tabs-mode RET’ or customize the mode hook.

What you probably want to do: Disable tabs globally (spaces only) and reactivate them for modes with smart tabs handling. For CC Mode:

    (setq-default indent-tabs-mode nil)
    (add-hook 'c-mode-common-hook
              (lambda () (setq indent-tabs-mode t)))

See GuessStyle for a package which attempts to determine the proper values for indent-tabs-mode and the indentation offset by analysing how the file is formatted. (Of course, since smart tabs are tab size-independent, such files do not have an offset.)

Adding Language Support

Support for other languages can be added by using the smart-tabs-add-language-support macro. All you need to do is pass in the unique symbol to identify the language, the name of the mode hook and a list of cons cells containing (indent-line-function . offset-variable). For example, if C++ mode was not provided by default it could be added as follows:

    (smart-tabs-add-language-support c++ c++-mode-hook
      ((c-indent-line . c-basic-offset)
       (c-indent-region . c-basic-offset)))

Note: All language support must be added before the call to smart-tabs-insinuate.

Whitespace Cleanup

If you use whitespace-cleanup either manually or as a before-save-hook, you have to disable a few items in whitespace-style. To do this run ‘M-x customize-variable RET whitespace-style RET’ and remove the entries for “Indentation SPACEs” and “SPACEs after TAB”.

Code

Note: This code does not need to be placed in your config file, this is legacy code here for the explanation of how the code works.

C-like languages

For CC Mode (C, C++, Java et al.), add

    (setq-default tab-width 4) ; or any other preferred value
    (setq cua-auto-tabify-rectangles nil)
    (defadvice align (around smart-tabs activate)
      (let ((indent-tabs-mode nil)) ad-do-it))
    (defadvice align-regexp (around smart-tabs activate)
      (let ((indent-tabs-mode nil)) ad-do-it))
    (defadvice indent-relative (around smart-tabs activate)
      (let ((indent-tabs-mode nil)) ad-do-it))
    (defadvice indent-according-to-mode (around smart-tabs activate)
      (let ((indent-tabs-mode indent-tabs-mode))
        (if (memq indent-line-function
                  '(indent-relative
                    indent-relative-maybe))
            (setq indent-tabs-mode nil))
        ad-do-it))
    (defmacro smart-tabs-advice (function offset)
      `(progn
         (defvaralias ',offset 'tab-width)
         (defadvice ,function (around smart-tabs activate)
           (cond
            (indent-tabs-mode
             (save-excursion
               (beginning-of-line)
               (while (looking-at "\t*\\( +\\)\t+")
                 (replace-match "" nil nil nil 1)))
             (setq tab-width tab-width)
             (let ((tab-width fill-column)
                   (,offset fill-column)
                   (wstart (window-start)))
               (unwind-protect
                   (progn ad-do-it)
                 (set-window-start (selected-window) wstart))))
            (t
             ad-do-it)))))
    (smart-tabs-advice c-indent-line c-basic-offset)
    (smart-tabs-advice c-indent-region c-basic-offset)

The rest of the page assumes the above is already in place.

JavaScript

For Js2Mode, add

    (smart-tabs-advice js2-indent-line js2-basic-offset)

Perl

For CPerlMode, add

    (smart-tabs-advice cperl-indent-line cperl-indent-level)

Python

For python.el, add

    (smart-tabs-advice python-indent-line-1 python-indent)
    (add-hook 'python-mode-hook
              (lambda ()
                (setq indent-tabs-mode t)
                (setq tab-width (default-value 'tab-width))))

For python-mode.el, add

    (smart-tabs-advice py-indent-line py-indent-offset)
    (smart-tabs-advice py-newline-and-indent py-indent-offset)
    (smart-tabs-advice py-indent-region py-indent-offset)

Smart tabs are, of course, fully Python compliant, as the interpreter ignores the leading whitespace of continuation lines. However, in accordance with PEP 8, you should probably use tabs only when maintaining old code.

Ruby

For RubyMode, add

    (smart-tabs-advice ruby-indent-line ruby-indent-level)
    (setq ruby-indent-tabs-mode t)

VHDL

For VhdlMode, add

    (smart-tabs-advice vhdl-indent-line vhdl-basic-offset)
    (setq vhdl-indent-tabs-mode t)

Code explanation

For those looking to implement this functionality in other modes (or even other editors), this section explains how the code above works. The method used is not specific to Emacs – any reasonably scriptable editor with syntactic auto-indentation can be made to indent with tabs and align with spaces.

The editor’s auto-indentation depends on two variables: the tab size and the indentation offset. The tab size is how many columns a tab occupies (more precisely, the distance between tab stops). The indentation offset is the standard amount of columns an indented line is shifted to the right (disregarding special cases like alignment of multi-line expressions).

If we have access to these two variables, we can separate “indentation” from “alignment” without knowing very much about the language itself. The idea is simple: temporarily set the tab size and indentation offset to a very large value before indenting. This is similar to calling GNU Indent with -i99 -ts99 -l999 (along with necessary style parameters).

For instance, say we want to indent the C function

    int f(int x,
    int y) {
    return g(x,
    y);
    }

We drastically “expand” the tab size and indentation offset (they must have the same value, so that one tab equals one indentation offset), and perform the indenting:

    int f(int x,
    ......int y) {
    ----------------------->return g(x,
    ----------------------->.........y);
    }

When we change the values back – say to 4 – we have

    int f(int x,
    ......int y) {
    --->return g(x,
    --->.........y);
    }

which is indeed tab size-independent.

This works because (1) setting the tab size and indentation offset to the same value ensures that all “instances” of the offset are encoded as tabs, (2) using a value that is larger than any amount of alignment forces spaces to be used for the rest. (A good value is probably a multiple of the maximum line length.)

Spaces before tabs

This situation could arise if the user accidently hits ‘SPC’ at the beginning of a line. Emacs’ indentation functions may not be able to detect and remove such spaces, because the perceived amount of indentation is unchanged.

    if(cond) {
    .-->return 0;

To be on the safe side, we see to that such spaces are weeded out before performing “expansion” and indentation:

    if(cond) {
    --->return 0;

Advising Emacs

In Emacs, the tab size is controlled by the buffer-local variable tab-width, while the indentation offset differs from mode to mode. The macro smart-tabs-advice constructs a wrap-around advice of the mode’s indentation function, which shadows these variables. That is, the original values of tab-width and the indentation offset are temporarily overwritten with a large value (fill-column). The advice also checks the beginning of the line for spaces preceding tabs; if any are found, they are deleted before indenting.

So, to add smart tabs support for a new language, all one has to do is investigate its major mode, identify the indentation functions and the offset variable, and pass them to smart-tabs-advice as such:

    (smart-tabs-advice language-indent-line language-basic-offset)

Also, ensure that the mode has tabs activated. Some modes, such as VhdlMode, control this with their own variable rather than through Emacs’ standard indent-tabs-mode.

The Net

The wheel appears to have been reinvented a number of times:

(As for non-Emacs implementations, Michael Geddes has written a Smart Tabs plugin for Vim. Also, IntelliJ IDEA has a “Smart Tabs” option, Anjuta’s tab handling is reportedly fixed in SVN, and the Kate editor’s indentation API distinguishes between “indentation” and “alignment”.)

Screenshot

SmartTabsScreenshot

SmartTabsScreenshot2

Questions

I tried to use smart-tabs-mode.el working with sh-mode and did not managed to, any idea? --DanielDehennin


CategoryIndentation