Thousands times a day you want to jump from a file to its test file (or to its CSS file, or to its header file, or any other related file) and just as many times you want to go back to the initial file. Jumping to related files is what this package is about.
How does a user specify that a file is related to a set of other files? One way is to create a function that takes a file as argument and returns a list of related filenames:
(defun my/related-files-jumper (file)
(let ((without-ext (file-name-sans-extension file)))
(list
(concat without-ext ".js")
(concat without-ext ".css"))))
(setq related-files-jumpers (list #'my/related-files-jumper))
The function my/related-files-jumper
is called a jumper. With
this setup, the command M-x related-files-jump
(that you may want to
bind, for example to C-x j
) will let the user jump from Foo.js
to
Foo.css
and back.
This is working good but has several limitations:
- If
Foo.css
is not in the same directory asFoo.js
or if you want to include test files which end with “-tests.js”,my/related-files-jumper
has to be modified in a non-obvious way or a complicated new jumper must be written and added torelated-files-jumpers
; - The function
my/related-files-jumper
has to be shared with all Emacs users working on the same project.
So related-files recommends another approach that is less powerful but much simpler. Here is another way to define the same jumper:
(setq related-files-jumpers
'((recipe :remove-suffix ".js" :add-suffix ".css")))
This jumper lets the user go from Foo.js
to Foo.css
. The package
will automatically inverse the meaning of :remove-suffix
and
:add-suffix
arguments so the user can also go from Foo.css
to
Foo.js
with this jumper (#Recipe-based jumpers for more information).
This kind of jumper can easily be shared with the members of a team
through a .dir-locals.el
file (info:emacs#Directory Variables).
Emacs-lisp developers can easily implement new kinds of jumpers beyond function-based and recipe-based ones (<a href=”#Implementing new kinds of jumpers”>#Implementing new kinds of jumpers).
The command M-x related-files-make
makes it easy to create a related
file if it doesn’t exist yet. It is also possible to fill newly
created files with content. If the content is always the same, a
string can be used to specify it:
(recipe :remove-suffix ".js"
:add-suffix ".css"
:filler ".my-class {}")
There are also auto-insert
-based and yasnippet-based ways to fill
new files (#Specifying fillers). New kinds of fillers can easily be
implemented (#Implementing new kinds of fillers).
The command M-x related-files-jump-or-make
shows you a list of
existing and non-existing related files. Choosing an existing one will
open it while choosing an existing one will first create (and possibly
fill it) then jump to it.
The simplest way to install related-files
is through MELPA. Then,
you should require the package and bind some of the provided commands,
e.g.,
(require 'related-files)
(global-set-key (kbd "C-x j") #'related-files-jump)
(global-set-key (kbd "C-x J") #'related-files-make)
;; you may also bind `related-files-jump-or-make'
;; If you want the recipe-based syntax (recommended):
(require 'related-files-recipe)
If you are a user of use-package, the configuration will look like this instead:
(use-package related-files
:bind (("C-x j" . related-files-jump)
;; you may also bind `related-files-jump-or-make'
("C-x J" . related-files-make)))
;; If you want the recipe-based syntax (recommended):
(use-package related-files-recipe
:demand t
:after related-files)
Before being able to use related-files, you have to specify how your
files relate to each other. There are two builtin ways to do that and
more ways can be implemented (<a href=”#Implementing new kinds of
jumpers”>#Implementing new kinds of
jumpers). Whatever way you choose, you should probably start with the
customize interface: M-x customize-variable RET
related-files-jumpers
. This interface will guide you with all the
possibilities and show you related documentation.
The two builtin ways to define jumpers are recipes (<a href=”#Recipe-based
jumpers”>#Recipe-based
jumpers) and functions (#Function-based jumpers). Whatever way you
choose, you have to put your jumpers in the related-files-jumpers
variable.
The simplest way to define a jumper is to use the recipe-based syntax.
Such a jumper defines transformations to apply to the current file name to get related file names.
For example, in a typical C project, .c
and .h
files are in the
same directory and a jumper could be defined as:
(recipe :remove-suffix ".c" :add-suffix ".h")
This is enough for the user to go from file.c
to file.h
. The
package will automatically inverse the meaning of :remove-suffix
and
:add-suffix
arguments so the user can also go from file.h
to
file.c
with this jumper.
Sometimes, related files are located in parallel hierarchies. For example, a typical Emacs-lisp project would need this jumper:
(recipe
:remove-suffix ".el"
:add-suffix "-tests.el"
:add-directory "test")
With this jumper the user could jump from “/project/src/lisp/calendar/parse-time.el” “/project/src/test/lisp/calendar/parse-time-tests.el” and back. How does the jumper know that you want related files “/src/test/lisp” and not “/src/lisp/test” or “/test/src/lisp”? The jumper ignores non-existing directories.
Related files may use different case, e.g., “Company.js” (capitalized “c”) and “company-tests.js” (un-capitalized “c”). In case-sensitive file systems, the recipe must specify how case is transformed:
(recipe
:remove-suffix ".js"
:add-suffix "-tests.js"
:case-transformer uncapitalize)
Here is the general form of these recipe-based jumpers:
(recipe
[:remove-suffix REMOVE-SUFFIX]
[:add-suffix ADD-SUFFIX]
[:add-directory ADD-DIRECTORY]
[:case-transformer TRANSFORMATION]
[:filler FILLER])
All five fields are optional but the first two should usually be there unless related files differ only by their directory or case. Modifications to the current filename to build the related one are applied in the order below:
:remove-suffix
- a string (e.g., “.el”) that the current filename should end with and that is going to be removed from it;
:add-suffix
- a string (e.g., “-tests.el”) that will be added at the end;
:case-transformer
- either
capitalize
oruncapitalize
to change the case of the filename; :add-directory
- a string (e.g., “test”) that is added next to each directory name in the current filename (only existing directories are taken into account).
The :filler
keyword specifies how to populate a related file when it
doesn’t exist yet (#Specifying fillers).
When recipe-based jumpers are not powerful enough for you, you can always use functions. A jumper can be a function accepting the current place as argument (a filename) and returning a (possibly-empty) list of (existing and non-existing) places the user might want to go to or create. Instead of returning a list, the jumper may also just return a place.
Here is an example function-based jumper definition:
(defun my/related-files-jumper (file)
(let ((without-ext (file-name-sans-extension file)))
(list
(concat without-ext ".js")
(concat without-ext ".css"))))
(setq related-files-jumpers (list #'my/related-files-jumper))
This jumper lets the user jump from Foo.js
to Foo.css
and back.
A filler can be associated to a function-based jumper by specifying
the filler in the related-files-filler
property of the function
(#Specifying fillers):
(put #'my/related-files-jumper 'related-files-filler filler)
Recipe-based jumpers and function-based jumpers are the two builtin ways to create jumpers but you may implement new kinds of jumpers (#Implementing new kinds of jumpers).
A filler is a way to populate a related file when
related-files-make
or related-files-jump-or-make
create one. A
jumper is responsible for declaring how to fill the files it creates.
Each kind of jumper has its own way to declare the associated
filler. For example, a recipe-based jumper needs a :filler
keyword
while a function-based one needs a related-files-filler
property. Nevertheless, the kinds of supported fillers are the same
regardless of the kind of jumper being defined. The remaining of this
section describes the builtin kinds of fillers. An Emacs-lisp
developer can easily implement more kinds (<a href=”#Implementing new kinds of
fillers”>#Implementing new kinds of
fillers).
If new files need the same content, a filler can be specified as a plain string, e.g.,
(recipe
:remove-suffix ".js"
:add-suffix ".css"
:filler ".my-class {}")
If new files are compatible with M-x auto-insert
, a filler can be
just the symbol auto-insert
:
(recipe
:remove-suffix ".el"
:add-suffix "-tests.el"
:filler auto-insert)
As soon as the user creates a new related-file from this jumper, the
auto-insert
function will be called.
If you use yasnippet, you can also specify a yasnippet-based filler in your jumper:
(recipe
:remove-suffix ".js"
:add-suffix ".stories.js"
:filler (yasnippet :name "stories"))
As soon as the user creates a new related-file from this jumper, the “stories” snippet will be inserted.
The previous section described the builtin kinds of jumpers and fillers. With a bit of Emacs-lisp knowledge, you can add new kinds of jumpers and fillers.
If you want to add a new kind of jumper, follow the steps below. You
can use related-files-recipe.el
as example.
- override
related-files-apply
; - optionally override
related-files-get-filler
if your new kind of jumper should support fillers; - optionally call
related-files-add-jumper-type
to specify a customization UI; - optionally add a function to
related-files-jumper-safety-functions
to indicate if jumpers are safe or unsafe to use (jumpers are considered unsafe by default).
If you want to add a new kind of filler, follow the steps below. You
can use the existing fillers in related-files.el
as example.
- override
related-files-fill
; - optionally call
related-files-add-filler-type
to specify a customization UI.