Subject: bug#79358: etags-regen-mode doesn't work for remote projects

From: Liu Hui <[email-masked]>
Date: Fri, 5 Sep 2025 22:07:48 +0800

On Wed, Sep 3, 2025 at 8:58 PM Dmitry Gutov <[email-masked]> wrote:
>
> Hi!
>
> On 03/09/2025 13:10, Liu Hui wrote:
> > So I changed shell-command-on-region/shell-command to
> > process-file-shell-command in the attached patch, which should make
> > etags-regen-mode work for both local and remote projects.
>
> Thanks for the patch, it's a good improvement.
>
> Specifically for call-process-region, could you try branching like the
> workaround helper xref--process-file-region? Maybe it could be copied
> verbatim even.
>
> Perhaps you could also try the puzzle described in the comment there? IF
> that observation still applies.

OK. I have added etags-regen--process-file-region in the patch. I
don't find there is a significant speed difference. Perhaps most time
is spent generating TAGS.

I changed the absolute file name to relative file name when generating
TAGS file if possible, because etags.el can handle relative file name
in both local and remote cases. etags.el uses expand-file-name for the
file name in TAGS file, and expand-file-name doesn't add remote part
for absolute file names.

Therefore, if the etags-regen-mode creates TAGS file outside of the
remote project root, etags.el cannot find remote files. Since the
problem lies in etags.el, it can be fixed separately.

Michael Albinus <[email-masked]> writes:

> Liu Hui <[email-masked]> writes:
>
> Hi,
>
> As said I don't use etags.el (and I haven't installed a tags program),
> but comment from cursory reading:
>
>> -                          (shell-quote-argument tags-file))))
>> +                          (shell-quote-argument (if remote
>> +                                                    (file-local-name tags-file)
>> +                                                  tags-file))))
>
> file-local-name returns always a proper result, for both remote and
> local files. So don't check with file-remote-p.

Done, thanks for pointing it out.

>> +         (tempfile (make-temp-file "etags-regen"))
>> +         (error-file (make-temp-file "etags-regen-err")))
>>      (with-temp-buffer
>>        (mapc (lambda (f)
>> -              (insert f "\n"))
>> +              (insert (if remote (file-local-name f) f) "\n"))
>>              files)
>> -      (shell-command-on-region (point-min) (point-max) command
>> -                               nil nil etags-regen--errors-buffer-name t))
>> +      (write-region nil nil tempfile))
>> +    (process-file-shell-command command tempfile (list nil error-file))
>
> tmpfile and error-file are local files, because created with
> make-temp-file. Why do you call process-file-shell-command then?
> call-process-shell-command would do the job.

When default-directory is remote, tmpfile contains file names on the
remote host, and process-file-shell-command can run etags remotely
with tmpfile as the input.
From c20aa72c8783254214ae4ecc97479b7bfaa0e23a Mon Sep 17 00:00:00 2001
From: Liu Hui <[email-masked]>
Date: Fri, 5 Sep 2025 21:32:47 +0800
Subject: [PATCH] Fix etags-regen-mode for remote projects (bug#79358)

* lisp/progmodes/etags-regen.el
(etags-regen--process-file-region): New function.
(etags-regen--tags-generate, etags-regen--append-tags): Use the
new helper function to run the program, and use relative file
name if TAGS file is in the project root.
(etags-regen--update-file): Use relative file name if possible.
---
 lisp/progmodes/etags-regen.el | 69 ++++++++++++++++++++++++++++-------
 1 file changed, 56 insertions(+), 13 deletions(-)

diff --git a/lisp/progmodes/etags-regen.el b/lisp/progmodes/etags-regen.el
index cde82956c24..ed18eb5a564 100644
--- a/lisp/progmodes/etags-regen.el
+++ b/lisp/progmodes/etags-regen.el
@@ -271,11 +271,43 @@ etags-regen--ignore-regexp
               ;; Either match a full name segment, or eos.
               "\\(?:/\\|\\'\\)"))))
 
+(defun etags-regen--process-file-region ( start end program
+                                          &optional output-buffer error-buffer
+                                          &rest args)
+  (let ((error-file (and error-buffer (make-temp-file "erpfr-err")))
+        infile)
+    (unwind-protect
+        (progn
+          (if (not (file-remote-p default-directory))
+              (if (and start end)
+                  (apply #'call-process-region
+                         start end program nil
+                         (list output-buffer error-file) nil args)
+                (apply #'call-process
+                       program nil (list output-buffer error-file) nil args))
+            (when (and start end)
+              (setq infile (make-temp-file "erpfr"))
+              (write-region start end infile nil 'silent))
+            (apply #'process-file
+                   program infile (list output-buffer error-file) nil args))
+          (when (and error-file
+                     (file-exists-p error-file)
+                     (< 0 (file-attribute-size (file-attributes error-file))))
+            (with-current-buffer (get-buffer-create error-buffer)
+              (erase-buffer)
+              (format-insert-file error-file nil)
+              (display-buffer (current-buffer)))))
+      (if infile (delete-file infile))
+      (if error-file (delete-file error-file)))))
+
 (defun etags-regen--tags-generate (proj)
   (let* ((root (project-root proj))
          (default-directory root)
          (files (etags-regen--all-files proj))
          (tags-file (etags-regen--choose-tags-file proj))
+         (fun (if (equal (file-name-directory tags-file)
+                         (expand-file-name root))
+                  #'file-relative-name #'file-local-name))
          (ctags-p (etags-regen--ctags-p))
          (command (format "%s %s %s - -o %s"
                           etags-regen-program
@@ -284,13 +316,19 @@ etags-regen--tags-generate
                                      " ")
                           ;; ctags's etags requires '-L' for stdin input.
                           (if ctags-p "-L" "")
-                          (shell-quote-argument tags-file))))
+                          (shell-quote-argument (file-local-name tags-file)))))
     (with-temp-buffer
       (mapc (lambda (f)
-              (insert f "\n"))
+              (insert (funcall fun f) "\n"))
             files)
-      (shell-command-on-region (point-min) (point-max) command
-                               nil nil etags-regen--errors-buffer-name t))
+      (with-connection-local-variables
+       (etags-regen--process-file-region (point-min)
+                                         (point-max)
+                                         shell-file-name
+                                         nil
+                                         etags-regen--errors-buffer-name
+                                         shell-command-switch
+                                         command)))
     (etags-regen--visit-table tags-file root)))
 
 (defun etags-regen--visit-table (tags-file root)
@@ -334,6 +372,9 @@ etags-regen--update-file
                              (get-file-buffer etags-regen--tags-file)))
          (relname (concat "/" (file-relative-name file-name
                                                   etags-regen--tags-root)))
+         (fun (if (equal (file-name-directory etags-regen--tags-file)
+                         (expand-file-name etags-regen--tags-root))
+                  #'file-relative-name #'file-local-name))
          (ignores etags-regen-ignores)
          pr should-scan)
     (save-excursion
@@ -347,7 +388,7 @@ etags-regen--update-file
           (set-buffer tags-file-buf)
           (setq should-scan t))
          ((progn (set-buffer tags-file-buf)
-                 (etags-regen--remove-tag file-name))
+                 (etags-regen--remove-tag (funcall fun file-name)))
           (setq should-scan t))))
       (when (and should-scan
                  (not (cl-some
@@ -376,15 +417,17 @@ etags-regen--remove-tag
 (defun etags-regen--append-tags (&rest file-names)
   (goto-char (point-max))
   (let ((options (etags-regen--build-program-options (etags-regen--ctags-p)))
+        (fun (if (equal (file-name-directory etags-regen--tags-file)
+                        (expand-file-name etags-regen--tags-root))
+                 #'file-relative-name #'file-local-name))
         (inhibit-read-only t))
-    ;; XXX: call-process is significantly faster, though.
-    ;; Like 10ms vs 20ms here.  But `shell-command' makes it easy to
-    ;; direct stderr to a separate buffer.
-    (shell-command
-     (format "%s %s -o - %s"
-             etags-regen-program (mapconcat #'identity options " ")
-             (mapconcat #'identity file-names " "))
-     t etags-regen--errors-buffer-name))
+    (with-connection-local-variables
+     (etags-regen--process-file-region
+      nil nil shell-file-name t etags-regen--errors-buffer-name
+      shell-command-switch
+      (format "%s %s -o - %s"
+              etags-regen-program (mapconcat #'identity options " ")
+              (mapconcat fun file-names " ")))))
   ;; FIXME: Is there a better way to do this?
   ;; Completion table is the only remaining place where the
   ;; update is not incremental.
-- 
2.25.1