[Top][All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Comparison of tools to search for related files

From: Damien Cassou
Subject: Comparison of tools to search for related files
Date: Mon, 05 Sep 2022 22:51:29 +0200


I'm implementing jumprel to jump from a file to related files (e.g., its
tests, its CSS, its .h header file…). During discussion for bug#57564,
Eli asked me

      How will this be different from what we already have:

      • the find-file.el package
      • the new command 'find-sibling-file'

I didn't know about these 2 packages so thank you very much for telling
me about them. Because I just learned about them, my description and
comparison below might be incomplete.

In the following I will compare the packages according to my 2 use

1. In the Emacs core code base, I want to jump from an elisp file (e.g.,
   `lisp/calendar/parse-time.el') to its test file (e.g.,
   `test/lisp/calendar/parse-time-tests.el', note the parallel folder
   hierarchy) and back.

2. In my JavaScript frontend project, I want to jump from a component
   file (e.g., `foo/MyComponent.js') to its Less file (e.g.,
   `foo/MyComponent.less', same folder and different extension) or to
   its UI test file (e.g., `foo/MyComponent.spec.component.js', same
   folder, same extension but different suffix) or to its non-UI test
   file (e.g., `test/foo/myComponent-tests.js', different casing and
   parallel folder hierarchy).

In both use cases, it would be nice to facilitate the creation of
non-existing files: for example, if a buffer visits `MyComponent.js' and
there is no `MyComponent.less', it would be great if Emacs could let me
create it from a list of non-existing related files.

I derive the following required features from these 2 use cases:

Parallel folder hierarchy
      It should be possible to jump from a file in `foo/bar/' to a file
      in `XX/foo/bar/' and back ("XX/" usually equals "test/" or
Choose candidate
      The user should be presented with a list of related file
      candidates to pick the one to jump to;
Case changing
      Some related files might have a different casing;
      The user should be presented with the related files that don't
      exist so they can be created automatically (very useful with
      parallel folder hierarchies). I don't consider that a must but a
      nice to have.

1 find-file.el

  find-file.el provides `ff-find-other-file' to jump from a file to a
  related file. How a file relates to another is done through
  `ff-other-file-alist' where elements can have 2 different forms:


  The first form above associates a filename regexp to a list of related
  file extensions. For example, `("\\.c\\'" (".h"))' associates a C file
  to its header file (it seems that C and C++ projects where the main
  target for this package).

  I didn't manage to configure find-file.el for parallel folder
  hierarchies. find-file.el provides `ff-search-directories' but it's
  not completely clear to me if this variable would make it possible to
  implement the use cases using the first form above (aka without
  relying on a function). I have the impression that it wouldn't.

  I managed to configure find-file.el for the cases when files are in
  the same folder as in most of the second use case:

  │ (("\\.spec.component\\.js\\'" (".js" ".less"))
  │  ("\\.less\\'" (".js" ".spec.component.js"))
  │  ("\\.js\\'" (".less" ".spec.component.js")))

  Unfortunately, only the first file matching from the EXTENSION list is
  used and the user is never presented a choice of candidate. Looking at
  the third line above, this means that if I'm in `MyComponent.js' and
  `MyComponent.less' exists, there is no way to go to

  To implement parallel folder hierarchy, it is possible to use the
  second as it allows for more flexibility. The Emacs core use case
  would be implemented with:

  │ (defun my/ff-other-file-for-emacs-core (filename)
  │   (save-match-data
  │     (if (string-match "test/lisp" filename)
  │         (let ((without-test-directory (replace-match "lisp" nil t 
  │           (list (replace-regexp-in-string "-tests\\.el$" ".el" 
  │       (let ((with-test-directory (string-replace "/lisp/" "/test/lisp/" 
  │         (list (replace-regexp-in-string "\\.el$" "-tests.el" 

  I have the impression that relying on elisp for such use cases will
  limit the usage of the package. For example, I haven't found any place
  in Emacs core code base or documentation where such a function would
  be given to facilitate the life of Emacs core contributors.

  find-file.el has a `ff-file-created-hook' variable to create and
  populate a file if no file exists. While a hook variable allows the
  user to do whatever it wants, it also means the user must write even
  more elisp code to do so.

  find-file.el has a `ff-special-constructs' to match import/include
  lines and open the imported file when cursor is on such a line. This
  seems completely unrelated to the rest of the code in find-file.el
  though and other find-file.el mechanisms are ignored in this case. I'm
  not sure why this code is here.

  Summary of find-file.el:

  Parallel folder hierarchy
        Supported through the creation of functions only. This limits
        the usage of this feature to elisp developers;
  Choose candidate
        find-file.el opens the first existing file and never let the
        user choose;
  Case changing
        Supported through the creation of functions only.
        A hook exists which means it is possible but is limited to elisp

  Beyond features, I found the code hard to read with very long
  functions, a lot of state mutation and no unit test. The code is
  around 800-line long.

2 find-sibling-file

  This "package" consists of an interactive function, a customizable
  variable and a helper function.

  The Emacs core use case can easily be implemented by configuring

  │ (("test/lisp/\\(.*\\)-tests\\.el$" "lisp/\\1.el")
  │  ("lisp/\\(.*\\)\\.el$" "test/lisp/\\1-tests.el"))

  This works great.

  Ignoring parallel folder hierarchy (implemented just like in the
  previous use case) and case changing, the second use case can be

  │ (("\\(.*\\)\\.spec.component\\.js\\'" "\\1.js" "\\1.less")
  │  ("\\(.*\\)\\.less\\'" "\\1.js" "\\1.spec.component.js")
  │  ("\\(.*\\)\\.js\\'" "\\1.spec.component.js" "\\1.less"))

  This works great but redundancy starts to be annoying. If you need to
  add `*.stories.js' files (as is the case for my JS project), you will
  start suffering.

  If several matching files exist, the user is prompted with a list of
  candidates to choose from.

  I haven't found a way to implement case changing and there is no
  creation mechanism either.

  When regexps are not enough, there is no fallback-to-function
  workaround as was the case with find-file.el. I don't doubt this can
  easily be implemented though.

  The package has a nice feature to let the user switch between the same
  file in two different projects (e.g., `emacs-src-27/lisp/abbrev.el'
  and `emacs-src-28/lisp/abbrev.el'). I don't need the feature but I can
  see how it can be useful.

  Summary of find-sibling-file:

  Parallel folder hierarchy
        Supported with (slightly redundant) regexps;
  Choose candidate
  Case changing

  Beyond features, the code is really simple and only 89-line long. It
  has no unit test though (yet?).

3 jumprel

  This is the package I'm working on. It provides a command to jump to a
  related file among existing candidates. It features a Domain-Specific
  Language (DSL) to describe the relation between files. For Emacs core,
  it would look like this


  │ (filename :remove-suffix ".el" :add-suffix "-tests.el" :add-directory 

  This line represents a jumper and must be added to
  `jumprel-jumpers'. This can be done in a `.dir-locals.el' file for
  example. This line is in my opinion much easier to understand and
  modify than the alternatives of the other two packages. Please note
  that this line works to go from a file to its test file and back: this
  limits the redundancy noticed above.

  The JavaScript UI use case would be implemented with these jumpers:

  │ (filename :remove-suffix ".js" :add-suffix "-tests.js" :add-directory 
"tests" :case-transformer uncapitalize)
  │ (filename :remove-suffix ".js" :add-suffix ".spec.component.js")
  │ (filename :remove-suffix ".js" :add-suffix ".less")
  │ (filename :remove-suffix ".js" :add-suffix ".stories.js")

  Note that the first line shows an example of using case

  When several files exist, the user is presented with a list of
  candidates just like `find-sibling-file'.

  `find-file.el' natively supports C and C++-based projects (see
  `cc-other-file-alist'). A similar configuration can be achieved with
  jumprel through such a simple jumper:

  │ (filename :remove-suffix ".c" :add-suffix ".h")

  If the DSL doesn't support your use case, it is possible to fallback
  to implementing a function, just like with find-file.el (I would
  prefer a patch to improve the DSL when it makes sense though). Another
  possibility is to define another DSL. For example, contrary to the
  other two packages, jumprel doesn't have any support for regexp-based
  definitions of file relations. This can easily be implemented by
  defining a new DSL and leveraging `find-sibling-file-search':

  │ (cl-defmethod jumprel-apply ((jumper (head regexp)) place)
  │   "Apply JUMPER to PLACE and return a new place or nil."
  │   (find-sibling-file-search
  │    place
  │    (list (cdr jumper))))

  With this in place, users can now specify exactly the same patterns as
  they would in `find-sibling-rules'. For example, the jumper below lets
  the user switch between the same file in two different projects (e.g.,
  `emacs-src-27/lisp/abbrev.el' and `emacs-src-28/lisp/abbrev.el'):

  │ (regexp "emacs/[^/]+/\\(.*\\)\\'" "emacs/.*/\\1")

  Additionally, jumprel provides a simple mechanism to declare how to
  populate files. For Emacs core, the `.dir-locals.el' file could

  │ (filename :remove-suffix ".el" :add-suffix "-tests.el" :add-directory 
"test" :filler auto-insert)

  The `:filler auto-insert' part indicates that `auto-insert' must be
  called when a test file is created. You can also specify a string
  instead of `auto-insert' to give a default content. The mechanism is
  extensible so I also implemented a way to populate a file based on a
  yasnippet snippet (in my `init.el' file):

  │ (cl-defmethod jumprel-maker-fill ((filler (head yasnippet)) 
&allow-other-keys &rest)
  │   (when-let* ((snippet (map-elt (cdr filler) :name)))
  │     (yas-expand-snippet (yas-lookup-snippet snippet major-mode))))

  Which means the user can now specify a yasnippet snippet in their
  `.dir-locals.el' file:

  │ (filename :remove-suffix ".js" :add-suffix ".spec.component.js" :filler 
(yasnippet :name "componentSpec"))

  Summary of jumprel:

  Parallel folder hierarchy
        Supported with a simple `:add-directory' directive.
  Choose candidate
  Case changing
        Supported with a simple `:case-transformer' directive.
        Supported with a simple `:filler' directive.

  Beyond features, jumprel's code is 403-line long but isn't fully
  documented yet. There are 227 lines of unit tests.

Damien Cassou

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill

reply via email to

[Prev in Thread] Current Thread [Next in Thread]