/related-files

Emacs package to easily find files related to the current one

Primary LanguageEmacs LispGNU General Public License v3.0GPL-3.0

related-files

MELPA Stable MELPA pipeline status

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.

1 Overview

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:

  1. If Foo.css is not in the same directory as Foo.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 to related-files-jumpers;
  2. 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.

2 Installation

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)

3 Specifying jumpers

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.

3.1 Recipe-based jumpers

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 or uncapitalize 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).

3.2 Function-based jumpers

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).

4 Specifying fillers

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).

4.1 String-based 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 {}")

4.2 Auto-insert-based fillers

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.

4.3 Yasnippet-based fillers

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.

5 Extending related-files

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.

5.1 Implementing new kinds of jumpers

If you want to add a new kind of jumper, follow the steps below. You can use related-files-recipe.el as example.

  1. override related-files-apply;
  2. optionally override related-files-get-filler if your new kind of jumper should support fillers;
  3. optionally call related-files-add-jumper-type to specify a customization UI;
  4. 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).

5.2 Implementing new kinds of fillers

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.

  1. override related-files-fill;
  2. optionally call related-files-add-filler-type to specify a customization UI.