From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Damien Cassou Newsgroups: gmane.emacs.devel Subject: Comparison of tools to search for related files Date: Mon, 05 Sep 2022 22:51:29 +0200 Message-ID: <87tu5lv92m.fsf@cassou.me> Mime-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="6385"; mail-complaints-to="usenet@ciao.gmane.io" Cc: Eli Zaretskii , Lars Ingebrigtsen To: emacs-devel@gnu.org Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Tue Sep 06 04:23:39 2022 Return-path: Envelope-to: ged-emacs-devel@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1oVOFW-0001Wg-Vs for ged-emacs-devel@m.gmane-mx.org; Tue, 06 Sep 2022 04:23:39 +0200 Original-Received: from localhost ([::1]:35148 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1oVOFV-0007sJ-Kr for ged-emacs-devel@m.gmane-mx.org; Mon, 05 Sep 2022 22:23:37 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]:47722) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1oVJ4a-0005Kt-05 for emacs-devel@gnu.org; Mon, 05 Sep 2022 16:52:01 -0400 Original-Received: from mail.choca.pics ([2001:910:1410:500::1]:60670) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1oVJ4X-0000GN-9m; Mon, 05 Sep 2022 16:51:59 -0400 Original-Received: from localhost (localhost.localdomain [IPv6:::1]) by mail.choca.pics (Postfix) with ESMTP id 78A12181929EF; Mon, 5 Sep 2022 22:51:32 +0200 (CEST) Original-Received: from mail.choca.pics ([IPv6:::1]) by localhost (mail.choca.pics [IPv6:::1]) (amavisd-new, port 10032) with ESMTP id JN1yZUjfYivE; Mon, 5 Sep 2022 22:51:31 +0200 (CEST) Original-Received: from localhost (localhost.localdomain [IPv6:::1]) by mail.choca.pics (Postfix) with ESMTP id 1ADAE181929F6; Mon, 5 Sep 2022 22:51:31 +0200 (CEST) X-Virus-Scanned: amavisd-new at choca.pics Original-Received: from mail.choca.pics ([IPv6:::1]) by localhost (mail.choca.pics [IPv6:::1]) (amavisd-new, port 10026) with ESMTP id SGEEbQAmKCHU; Mon, 5 Sep 2022 22:51:30 +0200 (CEST) Original-Received: from localhost (153.226.95.79.rev.sfr.net [79.95.226.153]) by mail.choca.pics (Postfix) with ESMTPSA id BDA35181929EF; Mon, 5 Sep 2022 22:51:30 +0200 (CEST) Received-SPF: pass client-ip=2001:910:1410:500::1; envelope-from=damien@cassou.me; helo=mail.choca.pics X-Spam_score_int: -18 X-Spam_score: -1.9 X-Spam_bar: - X-Spam_report: (-1.9 / 5.0 requ) BAYES_00=-1.9, SPF_HELO_NONE=0.001, SPF_PASS=-0.001, T_SCC_BODY_TEXT_LINE=-0.01 autolearn=ham autolearn_force=no X-Spam_action: no action X-Mailman-Approved-At: Mon, 05 Sep 2022 22:22:47 -0400 X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.io gmane.emacs.devel:294767 Archived-At: Hi, I'm implementing jumprel to jump from a file to related files (e.g., its tests, its CSS, its .h header file=E2=80=A6). During discussion for bug#575= 64, Eli asked me How will this be different from what we already have: =E2=80=A2 the find-file.el package =E2=80=A2 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 cases: 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 "tests/"); 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; Creation 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 =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90 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: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (REGEXP (EXTENSION...)) =E2=94=82 (REGEXP FUNCTION) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (("\\.spec.component\\.js\\'" (".js" ".less")) =E2=94=82 ("\\.less\\'" (".js" ".spec.component.js")) =E2=94=82 ("\\.js\\'" (".less" ".spec.component.js"))) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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 `MyComponent.spec.component.js'. 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: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (defun my/ff-other-file-for-emacs-core (filename) =E2=94=82 (save-match-data =E2=94=82 (if (string-match "test/lisp" filename) =E2=94=82 (let ((without-test-directory (replace-match "lisp" nil= t filename))) =E2=94=82 (list (replace-regexp-in-string "-tests\\.el$" ".el" = without-test-directory))) =E2=94=82 (let ((with-test-directory (string-replace "/lisp/" "/tes= t/lisp/" filename))) =E2=94=82 (list (replace-regexp-in-string "\\.el$" "-tests.el" wi= th-test-directory)))))) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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. Creation A hook exists which means it is possible but is limited to elisp developers. 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 =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95= =90=E2=95=90=E2=95=90 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 `find-sibling-rules': =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (("test/lisp/\\(.*\\)-tests\\.el$" "lisp/\\1.el") =E2=94=82 ("lisp/\\(.*\\)\\.el$" "test/lisp/\\1-tests.el")) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 This works great. Ignoring parallel folder hierarchy (implemented just like in the previous use case) and case changing, the second use case can be =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (("\\(.*\\)\\.spec.component\\.js\\'" "\\1.js" "\\1.less") =E2=94=82 ("\\(.*\\)\\.less\\'" "\\1.js" "\\1.spec.component.js") =E2=94=82 ("\\(.*\\)\\.js\\'" "\\1.spec.component.js" "\\1.less")) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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 Supported; Case changing Unsupported. Creation Unsupported. Beyond features, the code is really simple and only 89-line long. It has no unit test though (yet?). 3 jumprel =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90 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 =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (filename :remove-suffix ".el" :add-suffix "-tests.el" :add-dir= ectory "test") =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (filename :remove-suffix ".js" :add-suffix "-tests.js" :add-dir= ectory "tests" :case-transformer uncapitalize) =E2=94=82 (filename :remove-suffix ".js" :add-suffix ".spec.component.js") =E2=94=82 (filename :remove-suffix ".js" :add-suffix ".less") =E2=94=82 (filename :remove-suffix ".js" :add-suffix ".stories.js") =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 Note that the first line shows an example of using case transformation. 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: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (filename :remove-suffix ".c" :add-suffix ".h") =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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': =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (cl-defmethod jumprel-apply ((jumper (head regexp)) place) =E2=94=82 "Apply JUMPER to PLACE and return a new place or nil." =E2=94=82 (find-sibling-file-search =E2=94=82 place =E2=94=82 (list (cdr jumper)))) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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'): =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (regexp "emacs/[^/]+/\\(.*\\)\\'" "emacs/.*/\\1") =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 Additionally, jumprel provides a simple mechanism to declare how to populate files. For Emacs core, the `.dir-locals.el' file could contain: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (filename :remove-suffix ".el" :add-suffix "-tests.el" :add-dir= ectory "test" :filler auto-insert) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 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): =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (cl-defmethod jumprel-maker-fill ((filler (head yasnippet)) &al= low-other-keys &rest) =E2=94=82 (when-let* ((snippet (map-elt (cdr filler) :name))) =E2=94=82 (yas-expand-snippet (yas-lookup-snippet snippet major-mode)= ))) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 Which means the user can now specify a yasnippet snippet in their `.dir-locals.el' file: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80 =E2=94=82 (filename :remove-suffix ".js" :add-suffix ".spec.component.js"= :filler (yasnippet :name "componentSpec")) =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80 Summary of jumprel: Parallel folder hierarchy Supported with a simple `:add-directory' directive. Choose candidate Supported. Case changing Supported with a simple `:case-transformer' directive. Creation 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. --=20 Damien Cassou "Success is the ability to go from one failure to another without losing enthusiasm." --Winston Churchill