Tuesday, March 2, 2010

A pretty common task when editing c/c++ programs is to switch between
the source and header files (i.e. file.c or file.cpp and file.h). I
had a handy keyboard shortcut in vim to do this and, despite my
newfound appreciation for the concept of Emacs, this was one of
those things that in practice I was sorely missing. I actually
sort of expected it to be built in... M-x switch-from-source-to-header
or something, but I didn't find it (please let me know if it
exists!). So I did what you're supposed to do with Emacs, I forged my
own.
It turned out to be a pretty good exercise in learning elisp. I had
to figure out how to do if-then-else, a switch statement, some
simple logic, how to deal with strings, how to interact with emacs,
etc. For example, there's no "else" in elisp, instead you do
something like this.

(if (some condition)
(then action)
(else action)
)

However, the actions are limited to a single statement. If you
want to use more than one, you have to wrap it in a (progn ), like
this.

Anyways, here's my function to switch between source and header
files. I'm sure there's a better way to do it. There's a couple
shortcomings here - it only knows about ".h", ".c", and ".cpp"
extensions and if there is no ".c" or ".cpp" file corresponding to a
header, it opens up a ".cpp" file for you (maybe you wanted ".c"...).
There ought to be a way around the first problem - you could at least
specify a configurable table of extension associations. The second
problem may be more of a design decision problem. It works pretty
well for me - I develop primarily in c++, so assuming ".cpp" is fine
for me. If you want the opposite behavior, just swap ".cpp" and ".c"
so they are checked in the opposite order. Of course you could ask
for an input, but that goes beyond the scope of what I wanted.
Another open question for me is how elisp treats local and global
variables. I've purposely chosen short, unintelligible variables names
here to avoid clobbering some other global variable. I think the "let"
command has something to do with this, but I didn't have time to work
it out.

(defundts-switch-between-header-and-source ()
"Switch between a c/c++ header (.h) and its corresponding source (.c/.cpp)."
(interactive)
;; grab the base of the current buffer's file name
(setq bse (file-name-sans-extension buffer-file-name))
;; and the extension, converted to lowercase so we can
;; compare it to "h", "c", "cpp", etc
(setq ext (downcase (file-name-extension buffer-file-name)))
;; This is like a c/c++ switch statement, except that the
;; conditions can be any true/false evaluated statement
(cond;; first condition - the extension is "h"
((equal ext "h")
;; first, look for bse.c
(setq nfn (concat bse ".c"))
(if (file-exists-p nfn)
;; if it exists, either switch to an already-open
;; buffer containing that file, or open it in a new buffer
(find-file nfn)
;; this is the "else" part - note that if it is more than
;; one line, it needs to be wrapped in a (progn )
(progn;; look for a bse.cpp
(setq nfn (concat bse ".cpp"))
;; likewise
(find-file nfn)
)
)
)
;; second condition - the extension is "c" or "cpp"
((or (equal ext "cpp") (equal ext "c"))
;; look for a corresponding bse.h
(setq nfn (concat bse ".h"))
(find-file nfn)
)
)
)
(global-set-key (kbd "C-c s") 'dts-switch-between-header-and-source)