Saturday, December 15, 2012

Aligning assignments to the same column




I've recently written 2 Emacs interactive commands for editing programs. The first command is a function that aligns a series of assignment operators written on separate consecutive lines, so that the equal signs ("=") are placed on the same column, which looks quite pretty and is sometimes preferred in certain coding conventions:


Unaligned
Aligned



The second command is useful when you're programming in C and use multiline macros (#defines) heavily.  It appends a backslash to the end of each line that constitutes the macro definition, and those backslashes are properly aligned to appear on the same column.  When you edit your macro definition, the slashes cease to be aligned; it is then sufficient to call this command again, and it will realign slashes as you want:



Aligning assignments

Here's the code that aligns assignments:

(defun eld-align-assignments ()

  (interactive)
  (save-excursion
    (let ((markers ())           ;retain all "=" positions here.
          (col-offsets ())       ;corresponding column offsets
          (longest-offset 0))
      (unwind-protect
          (flet
              ((examine-current-line ()
                 ;; Return the position of "=" if the current
                 ;; line looks like an assignment.  Otherwise,
                 ;; return nil.  Point is not moved.
                 (assert (bolp))
                 (when (looking-at "\\(?:\\s-\\|\\sw\\|\\s_\\|\\s.\\)*\\(=\\)")
                   (push (copy-marker (match-beginning 1)) markers)
                   (let ((col-offset (- (match-beginning 1) (point))))
                     (callf max longest-offset col-offset)
                     (push col-offset col-offsets)
                     t))))
            (forward-line 0)
            (examine-current-line)
            ;; if the first line did not match, it doesn't tell us anything: a
            ;; user can launch the command from the following line.
            (loop 
             do
             (when (= (forward-line -1) -1) (return)) ;reached (point-min)
             (unless (examine-current-line) (return)))
            ;; okay, now we have longest-offset and know how many spaces to put
            ;; at each marker
            (loop
             for marker in markers
             for col-offset in col-offsets
             do
             (goto-char marker)
             (insert-char ?\s (- longest-offset col-offset))))
        (dolist (m markers)
          (set-marker m nil))))))


The logic is quite simple-minded: examine lines to the top of the current
one trying to distinguish those which look like assignments. In this
respect, the algorithm is not sophisticated, as you can see: we're just
matching a regular expression from the beginning of the line, and that's
it.  This should suffice for simple cases; however, assignments to arrays
with subscripts that contain "=" themselves may not work properly, for example.
This is not very common, though, as in most situations assignments are done to
simple variables.

Once a line has been recognized as an assignment, we're placing a mark
before the equal sign; we also remember the longest column where an equal
sign appears.  Having determined the longest-column, the only thing left to
do is to pad each = with spaces so that all they move to the same column --
the longest-column.

Note: In this snippet I used flet macro which I was recommended against in one of
my previous posts.  I did that because I want people to be able to just copy-paste
that command and have it work for them.  For those who want to take the long way,
consider throwing flet out.


Aligning slashes

(defun eld-realign-c-macro/region (start end) "Append backslash to each line that ends within the given region. Backslahes are aligned to the same column as is usually done in C. If a line already ends in a backslash, this is handled gracefully." (interactive "r") (setq end (copy-marker end t)) (unwind-protect (save-excursion (goto-char start) (let ((max-column 0)) (loop (end-of-line 1) (unless (< (point) end) (return)) ;; check whether preceding char is backslash (when (= (preceding-char) ?\\) (delete-char -1)) (delete-horizontal-space t) ;delete backward whitespace only (setq max-column (max max-column (current-column))) (forward-char) ;move to the next line, 1 char is ok ) ;; now we've determined max-column. proceed with aligning slashes (goto-char start) (loop (end-of-line 1) (unless (< (point) end) (return)) (move-to-column max-column t) (insert-char ?\\ 1) (forward-char)))) (set-marker end nil))) (defun eld-realign-c-macro () "Like `eld-make-c-macro/region' but determines the region itself. The function does so by calling `c-beginning-of-macro' and `c-end-of-macro'." (interactive) (eld-realign-c-macro/region (save-excursion (c-beginning-of-macro) (point)) (save-excursion (c-end-of-macro) (point)))) 

The algorithm resembles the previous one: examine each line (this time the
end of it) and remember the column number where a backslash should be
placed.  Then find maximum of these numbers and place backslashes in each
line there.

To determine the beginning and end of a macro definition, functions
c-beginning-of-macro and c-end-of-macro are used.  They are the standard
ones from the C mode.

There's quite an interesting idiom in the function eld-realign-c-macro
above: save excursion, then execute code that moves point at the desired
location, and return that location by calling (point).  This could be
highlighted as a special macro:

(defmacro point-would-be (&rest body)
  `(save-excursion
     (progn ,@body)
     (point)))

I hope someone will find these things useful.  Thanks for attention !