Indent with tabs, align with spaces

There have been countless discussions on whether you should use tabs or spaces to indent your code. Until recently I have been a fan of spaces. But why settle with spaces OR tabs? After some discussions with my good friend Ali some time ago, I agreed with myself that I actually prefer mixing tabs and spaces. The idea is of course to get the best of both worlds: indenting with tabs in order to respect each reader’s/developer’s preferred indentation width, and to align with spaces when expressions expand over multiple lines in order to ensure readability for different tab widths. The image below illustrates where I use tabs and where I uses spaces. This way the source code will stay readable even if it’s written with tabs of width 2 and read in an environment that has tabs width of 8.

Indent with tabs, align with spaces

I assume this is nothing new, and hopefully there is a lot of persons out there using this style. However, I was not able to find editors that can indent the code like this automatically nor any extensions to Emacs that does this. So I sat down and wrote a Lisp snippet in order to make Emacs automatically fill in spaces and tabs just the way I want it upon indentation. It uses functions from the c-mode to help to decide where to put spaces and tabs, so I hope it works for other formatting styles than just mine. (Sorry for the poor syntax highlighting, but there seems to be no support for lisp code.)

;; See http://www.gnu.org/software/emacs/manual/html_node/ccmode/Syntactic-Symbols.html
(defvar c-elements-to-align-with-spaces
  (list 'func-decl-cont
        'topmost-intro-cont
        'arglist-cont
        'arglist-cont-nonempty
        'statement-cont
        'c
        'inher-cont
        'member-init-cont
        'template-args-cont
        'objc-method-args-cont
        'objc-method-call-cont)
  "List of syntactic elements that should be aligned with spaces.
If you find an element you want to align with spaces but is not handled here,
find the syntactic element with C-c C-s or M-x c-show-syntactic-information
and simply add it to the list.")

(defun c-context-continuation-p (context)
  "Returns t if the given context is part of a continuation, i.e.
it should be aligned with spaces. The syntactic elements defined
as being a part of a continuation is defined by the variable
c-elements-to-align-with-spaces."
  (let ((continuation nil))
    (dolist (elem c-elements-to-align-with-spaces continuation)
      (when (assq elem context)
        (setq continuation t)))))

(defun c-indent-align-with-spaces-hook ()
  "If indent-tabs-mode is nil this function does nothing. If
indent-tabs-mode is enabled and if current indentation is an
alignment operation, this function will format the line so that
tabs are used until the indent level of the previous line and use
spaces for the rest (the aligment)."
  (interactive)
  (when indent-tabs-mode
    (let ((context c-syntactic-context)
          (curr-indent (current-indentation))
          (base-indent nil))
      (when (c-context-continuation-p context)
        (save-excursion
          ;; Find indentation of nearest not-continuation context
          (do ()
              ((not (c-context-continuation-p context)))
            (goto-char (c-langelem-pos (car context)))
            (setq context (c-guess-basic-syntax)))
          (setq base-indent (current-indentation)))
        ;; Untabify region between base indent and current indent
        (let ((end (point)))
          (save-excursion
            (while (> (current-column) base-indent)
              (backward-char))
            (untabify (point) end)))
        ;; We might need to adjust the marker to a more correct/practical
        ;; position.
        (when (= (current-column) base-indent)
          (back-to-indentation))))))

;; Activate the new indentation style
(setq c-special-indent-hook nil)
(add-hook 'c-special-indent-hook 'c-indent-align-with-spaces-hook)
About these ads

13 responses to “Indent with tabs, align with spaces

  1. Dude, if this works like you say it does it needs to replace the current indentation scheme in the Emacs distro!

    I’m having problems getting it to work… I’m going to keep working at it but does tab-width need to be set to anything special? Perhaps a multiple of standard-indent or c-indent-level or c-basic-offset? Does it need to be loaded before or after anything else in my .emacs file? Would you be willing to post your .emacs and custom-file files?

  2. Oh, this is nice. I set all of standard-indent, c-indent-level, c-basic-offset, and tab-width to the same thing (3). Only thing I’ve noticed so far is if the for loop’s three expressions are put on separate lines it uses tabs instead of spaces for the alignment. It looks like that’s more Emacs’ fault, though, as it’s only recognizing it as a “statement 138″ and not something more specific.

    Great job – thank you!!!

  3. Hi Nathan, good to hear that you like the idea. I’m happy that someone agree with me :)

    You might be right about that these variables need be the same. I have all set to 4 (except c-indent-level which does not seem to be defined), and I have never tried with any different values. If I find time for it, I will try look into it to save some time for the next person than stumbles upon this post. Thanks for you feedback.

  4. Brilliant idea. Why this isn’t default in emacs is also a mystery to me.
    My emacs 22.1.1 is running into the following error:
    save-excursion: Symbol’s function definition is void: do
    Do you by any chance know where this ‘do’ function can be loaded from? Or how it is defined?
    Looking forward to get my code indent-aligned in a flexible way.

  5. Hi Markus. I have the `do’ function available without loading anything except what is default on Ubuntu. The documentation states that `do’ is a Common Lisp macro, so I believe it should be available without doing anything special. However, I use the snapshot of Emacs 23.

    What you can do in order to debug a bit is to try to type `C-h f do’ to see if you get the documentation of the function. Or try to run the simple snippet (do () (nil) (message "hello world")) from within Emacs. Put the marker behind the last parentheses and hit `C-x e’. “hello world” should be output in the message area. (`C-g’ to abort the loop.)

    If the above works, you should try to run the alignment code from a clean .emacs file. Save the code found at http://pastebin.com/f20a6daf1 into a separate file, e.g. `.emacs-indent-debug’, and start emacs with `emacs -l .emacs-indent-debug’. Try to open a c-file and see if the indentation works now. Good luck!

  6. Hi Stianse,
    Thanks for your help. “Common Lisp” was the answer.
    To get the ‘do’ function, add
    (require ‘cl)
    either in your ~/.emacs, or at the start of the script.
    And it works perfect.
    Thanks for your great job.

  7. A more general method is to temporarily change the “tab width” and the “indentation offset” to a very large value while indenting. E.g., we go from

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

    to (indenting with a large value)

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

    to (changing the value back)

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

    This works because (1) setting the tab width 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.

    Example Emacs code:

    (defadvice c-indent-line (around smart-tabs activate)
    (setq tab-width tab-width)
    (let ((indent-tabs-mode t)
    (tab-width fill-column)
    (c-basic-offset fill-column))
    ad-do-it))

    It is more general because it does not require us to know very much about the language itself. E.g., for Perl, just replace “c-indent-line” with “cperl-indent-line” and “c-basic-offset” with “cperl-indent-level”.

    See http://www.emacswiki.org/emacs/SmartTabs for more details and examples.

  8. @marius: Nice approach. Good to see that the solutions for this indentation problem are improving, that is becoming smarter/simpler and more general. Haven’t got around to test SmartTabs myself yet, but I will.

  9. By the way, could you fix the formatting of my previous comment? :) A fixed-width font is required to line things up.

  10. Done. The indentation in the lisp snippet didn’t want to be displayed properly though, but I figured that’s not so important.

  11. “Tab indent & space align” is the same conclusion I came to after weighing up all the options. Once I acknowledged that “indentation” and “alignment” are actually 2 different problems, I realized that it was perfectly fine to have 2 different solutions.

  12. Super great article. Really.

  13. I’ve been a fan of this style for quite a while too. However, it is a bit difficult for other developers to understand or notice immediately. If you’re working on a team, it’s not uncommon to have other developers unintentionally mess up your code because their editor isn’t properly configured.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s