2

Here's a simple use case:

I open bar/foo/file in a buffer and I'm working. In eshell, I do mv bar/foo/file bar/baz/foo/file. Now emacs can no longer recognize where to save and doesn't save it properly (it asks to make the directory foo to do so since it thinks it's still working with bar/foo/file). How can I make it so it's internally updated?

For example:

⛔ Error (auto-save): Auto-saving file: Opening output file: No such file or directory, /bar/foo/#file#
2
  • NOTE: all the answers here are only partial: When you "move" a file in the OS: - the content of the file is saved to the new filename - the initial file is removed. All the propositions here: - set-visited-file-name - buffer-file-name - C-x C-w are creating the new file and "redirecting" the future saves to this new file. BUT they lack the removal of the initial file. Which is what OP (and I) are looking for.
    – liar666
    Dec 13, 2023 at 10:09
  • I believe you've misunderstood the question. If you want Emacs to perform a filesystem rename, you can use the rename-file command. (In the OP's case they were using the mv command in eshell which ultimately calls rename-file). If you are currently editing that file in a buffer, you can use the rename-visited-file command.
    – phils
    Dec 13, 2023 at 13:32

2 Answers 2

4

I could be wrong - if so, someone will correct me. But I think this is the case:

How can you expect Emacs to know where the file it read was moved to, if you move it outside Emacs? Same thing for if you delete the file. (But maybe eshell lets the rest of Emacs know about the move somehow?)


But what you can do, if you have edited the buffer since the file on disk was moved, is use C-x C-w (command write-file) to write the (edited, up-to-date) buffer content to the new file location. For that, you'll need to give Emacs the new location (you can use completion), and you'll likely need to confirm that you want to overwrite the existing (out-of-date) file at the new location.

3
  • Got it. And C-x C-w guarantees future writes go there? Jul 19, 2023 at 1:56
  • 1
    Yes; it changes C-h v buffer-file-name to the new path, so future C-x C-s will write to the new path as well.
    – phils
    Jul 19, 2023 at 3:40
  • A similar idea is to use set-visited-file-name. That will not save the buffer to the new file immediately, so when you do save it, you will have to go through the confirmation dialogs, but the final result is the same.
    – NickD
    Jul 19, 2023 at 11:46
2

Generally

To do this you would need to have OS file watchers in operation for every directory of interest (not just the files Emacs is visiting, but the directories to which the renamed file could potentially be written to), so that they could notify Emacs if anything changed. Even then, renaming probably isn't something they know about -- the watcher may simply see a file deletion and a file creation which you would need to know how to interpret as a file rename by examining those events (I believe this is true for inotify, and I've no idea whether any alternative would be different).

Alternatively, you could use custom wrappers for the programs which perform the renaming such they each notify Emacs explicitly. You would need to do this for every program which might rename a file that Emacs was visiting -- but maybe in practice you only care about mv and could make yourself a wrapper for that. You would need to implement some kind of protocol for communicating with Emacs to give it his information, of course -- AFAIK nothing like this already exists.

Eshell

In your specific case of eshell... well that's more interesting: eshell commands can already be lisp, and they have access to your buffers and to the file arguments, so that case becomes more do-able, but it still won't be trivial to make something robust.

As it happens, mv is a lisp command in eshell. Or at least I see it as:

$ which mv
eshell/mv is a byte-compiled Lisp function in ‘em-unix.el’.

Using trace-function for eshell/mv and running mv gets me the likes of:

======================================================================
1 -> (eshell/mv "foo" "bar")
1 <- eshell/mv: nil
======================================================================
1 -> (eshell/mv "--help")
1 <- eshell/mv: !non-local\ exit!
======================================================================
1 -> (eshell/mv "-f" "foo" "bar")
1 <- eshell/mv: nil

Which doesn't tell me which args are files; what the absolute paths for the files are (although a simple (expand-file-name ARG) against the eshell buffer's default-directory is probably all that's needed); nor does it tell us whether a rename succeeded -- or was even attempted (here the first succeeded and the third failed because foo didn't exist any more).

Ultimately this calls rename-file which will know whether or not the renaming was successful. Or rather, will signal an error if it fails.

A bit more tracing shows other examples like:

======================================================================
1 -> (eshell/mv "-n" "foo" "bar" "dir/")
| 2 -> (eshell-shuffle-files "mv" "moving" ("foo" "bar") "dir/" rename-file nil t)
| 2 <- eshell-shuffle-files: nil
1 <- eshell/mv: nil
======================================================================
1 -> (eshell/mv "foo" "bar" "dir/")
| 2 -> (eshell-shuffle-files "mv" "moving" ("foo" "bar") "dir/" rename-file nil t)
| | 3 -> (rename-file "foo" "/tmp/dir/foo" t)
| | 3 <- rename-file: nil
| | 3 -> (rename-file "bar" "/tmp/dir/bar" t)
| | 3 <- rename-file: nil
| 2 <- eshell-shuffle-files: nil
1 <- eshell/mv: nil
======================================================================
1 -> (eshell/mv ("dir/bar" "dir/foo") ".")
| 2 -> (eshell-shuffle-files "mv" "moving" ("dir/bar" "dir/foo") "." rename-file nil t)
| | 3 -> (rename-file "dir/bar" "/tmp/bar" t)
| | 3 <- rename-file: nil
| | 3 -> (rename-file "dir/foo" "/tmp/foo" t)
| | 3 <- rename-file: nil
| 2 <- eshell-shuffle-files: nil
1 <- eshell/mv: nil
======================================================================
1 -> (eshell/mv "bar" "baz")
| 2 -> (eshell-shuffle-files "mv" "moving" ("bar") "baz" rename-file nil t)
| | 3 -> (rename-file "bar" "baz" t)
| | 3 <- rename-file: nil
| 2 <- eshell-shuffle-files: nil
1 <- eshell/mv: nil
======================================================================
1 -> (eshell/mv "bar" "baz")
| 2 -> (eshell-shuffle-files "mv" "moving" ("bar") "baz" rename-file nil t)
| | 3 -> (rename-file "bar" "baz" t)
| | 3 <- rename-file: !non-local\ exit!
| 2 <- eshell-shuffle-files: nil
1 <- eshell/mv: nil

So I think you'd need to advise the rename-file primitive for starters. That might not be reliable for the following reason, but in practice I suspect it's always a lisp call when eshell/mv is the cause.

It is possible to advise a primitive (*note What Is a Function::), but one should typically not do so, for two reasons. Firstly, some primitives are used by the advice mechanism, and advising them could cause an infinite recursion. Secondly, many primitives are called directly from C, and such calls ignore advice; hence, one ends up in a confusing situation where some calls (occurring from Lisp code) obey the advice and other calls (from C code) do not.

-- C-hig (elisp)Advising Named Functions

If you wanted to restrict the behaviour to eshell then you'll need to figure that part out as well. I can see that by the time rename-file is called, this-command has a value eshell-send-input so that doesn't inform us that eshell/mv was used, but maybe that's fine. If so, your rename-file advice could test that and only act when this-command was eshell-send-input. Otherwise you'll need to establish which eshell command is running (I'm sure that's simple, but I can't see it immediately).

So with the most minimal of testing, and absolutely no guarantees, here's a first-pass implementation of that:

(define-advice rename-file (:after (file newname &optional ok-if-already-exists) my-move-buffer)
  "Update `buffer-file-name' in matching buffers.

Advice for `rename-file'.  To remove:
\(advice-remove \\='rename-file \\='rename-file@my-move-buffer)"
  (when (eq this-command 'eshell-send-input)
    (let ((oldname (expand-file-name file))
          (newname (expand-file-name newname)))
      (unless (file-exists-p oldname)
        (dolist (buf (buffer-list))
          (with-current-buffer buf
            (when (and buffer-file-name
                       (string= buffer-file-name oldname))
              (setq buffer-file-name newname)
              (setq default-directory (file-name-directory newname))
              (when (string= (buffer-name) (file-name-nondirectory oldname))
                (rename-buffer (file-name-nondirectory newname) t)))))))))

Tracing the advice confirms that it never gets called if rename-file signals an error:

======================================================================
1 -> (eshell/mv "dir/new" "/")
| 2 -> (eshell-shuffle-files "mv" "moving" ("dir/new") "/" rename-file nil t)
| | 3 -> (rename-file "dir/new" "/new" t)
| | 3 <- rename-file: !non-local\ exit!
| 2 <- eshell-shuffle-files: nil
1 <- eshell/mv: nil
======================================================================
1 -> (eshell/mv "dir/new" "foo")
| 2 -> (eshell-shuffle-files "mv" "moving" ("dir/new") "foo" rename-file nil t)
| | 3 -> (rename-file "dir/new" "foo" t)
| | | 4 -> (rename-file@my-move-buffer "dir/new" "foo" t)
| | | 4 <- rename-file@my-move-buffer: nil
| | 3 <- rename-file: nil
| 2 <- eshell-shuffle-files: nil
1 <- eshell/mv: nil

I'm not recommending that you use this, but it was an interesting exercise and it's probably a reasonable starting point if you still want to pursue this idea.

1
  • Per @NickD's comment, you can call (set-visited-file-name newname t t) in place of the individual manipulations performed in the example advice (and probably should do -- it deals with additional things like autosave filenames).
    – phils
    Jul 19, 2023 at 12:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.