Vim vs. Emacs: The Working Directory
Vim and Emacs have different internals models for the current working directory, and these models influence the overall workflow for each editor. They decide how files are opened, how shell commands are executed, and how the build system is operated. These effects even reach outside the editor to influence the overall structure of the project being edited.
In the traditional unix model, which was eventually adopted
everywhere else, each process has a particular working directory
tracked by the operating system. When a process makes a request to the
operating system using a relative path — a path that doesn’t begin
with a slash — the operating system uses the process’ working
directory to convert the path into an absolute path. When a process
forks, its child starts in the same directory. A process can change
its working directory at any time using
most programs never need to do it. The most obvious way this system
call is exposed to regular users is through the shell’s built-in
Vim’s spiritual heritage is obviously rooted in vi, one of the classic
unix text editors, and the most elaborate text editor standardized by
POSIX. Like vi, Vim closely follows the unix model for working
directories. At any given time Vim has exactly one working directory.
Shell commands that are run within Vim will start in Vim’s working
directory. Like a shell, the
cd ex command changes and queries Vim’s
Emacs eschews this model and instead each buffer has its own working
directory tracked using a buffer-local variable,
Emacs internally simulates working directories for its buffers like an
operating system, resolving absolute paths itself, giving credence to
the idea that Emacs is an operating system (“lacking only a decent
editor”). Perhaps this model comes from ye olde lisp machines?
In contrast, Emacs’
M-x cd command manipulates the local variable
and has no effect on the Emacs process’ working directory. In fact,
Emacs completely hides its operating system working directory from
Emacs Lisp. This can cause some trouble if that hidden working
directory happens to be sitting on filesystem you’d like to unmount.
Vim can be configured to simulate Emacs’ model with its
option. When set, Vim will literally
chdir(2) each time the user
changes buffers, switches windows, etc. To the user, this feels just
like Emacs’ model, but this is just a convenience, and the core
working directory model is still the same.
Single instance editors
For most of my Emacs career, I’ve stuck to running a single, long-lived Emacs instance no matter how many different tasks I’m touching simultaneously. I start the Emacs daemon shortly after logging in, and it continues running until I log out — typically only when the machine is shut down. It’s common to have multiple Emacs windows (frames) for different tasks, but they’re all bound to the same daemon process.
While with care it’s possible to have a complex, rich Emacs
configuration that doesn’t significantly impact Emacs’ startup time, the
general consensus is that Emacs is slow to start. But since it has a
really solid daemon, this doesn’t matter: hardcore Emacs users only ever
start Emacs occasionally. The rest of the time they’re launching
emacsclient and connecting to the daemon. Outside of system
administration, it’s the most natural way to use Emacs.
The case isn’t so clear for Vim. Vim is so fast that many users fire
it up on demand and exit when they’ve finished the immediate task. At
the other end of the spectrum, others advocate using a single
instance of Vim like running a single Emacs daemon. In my
initial dive into Vim, I tried the single-instance, Emacs way of
doing things. I set
autochdir out of necessity and pretended each
buffer had its own working directory.
At least for me, this isn’t the right way to use Vim, and it all comes
down to working directories. I want Vim to be anchored at the
project root with one Vim instance per project. Everything is
smoother when it happens in the context of the project’s root
directory, from opening files, to running shell commands (
particular), to invoking the build system. With
actions are difficult to do correctly, particularly the last two.
Invoking the build
I suspect the Emacs’ model of per-buffer working directories has, in a
Sapir-Whorf sort of way, been responsible for leading developers
towards poorly-designed, recursive Makefiles. Without a global
concept of working directory, it’s inconvenient to invoke the build
M-x compile) in some particular grandparent directory that
is the root of the project. If each directory has its own Makefile, it
usually makes sense to invoke
make in the same directory as the file
Over the years I’ve been reinventing the same solution to this
problem, and it wasn’t until I spent time with Vim and its alternate
working directory model that I truly understood the problem. Emacs
itself has long had a solution lurking deep in its bowels, unseen by
daylight: dominating files. The function I’m talking about is
(locate-dominating-file FILE NAME)
Look up the directory hierarchy from FILE for a directory containing NAME. Stop at the first parent directory containing a file NAME, and return the directory. Return nil if not found. Instead of a string, NAME can also be a predicate taking one argument (a directory) and returning a non-nil value if that directory is the one for which we’re looking.
The trouble of invoking the build system at the project root is that Emacs doesn’t really have a concept of a project root. It doesn’t know where it is or how to find it. The vi model inherited by Vim is to leave the working directory at the project root. While Vim can simulate Emacs’ working directory model, Emacs cannot (currently) simulate Vim’s model.
Instead, by identifying a file name unique to the project’s root (i.e.
a “dominating” file) such as
locate-dominating-file can discover the project root. All that’s
left is wrapping
M-x compile so that
temporarily adjusted to the project’s root.
That looks very roughly like this (and needs more work):
(defun my-compile () (interactive) (let ((default-directory (locate-dominating-file "." "Makefile"))) (compile "make")))
It’s a pattern I’ve used again and again and again, working against the same old friction. By running one Vim instance per project at the project’s root, I get the correct behavior for free.