diff options
| author | Michael Albinus | 2019-11-20 13:45:30 +0100 |
|---|---|---|
| committer | Michael Albinus | 2019-11-20 13:45:30 +0100 |
| commit | 035931777bd89b939436fd1d8a2b8d5a80ede095 (patch) | |
| tree | b0ce714884bc106eaa95415754e807cb40c761e6 /lisp | |
| parent | 5c5c1b559313d06aab6516ff1b1acf1da3a01c7d (diff) | |
| download | emacs-035931777bd89b939436fd1d8a2b8d5a80ede095.tar.gz emacs-035931777bd89b939436fd1d8a2b8d5a80ede095.zip | |
Add renaming of remote buffer file names to Tramp
* doc/misc/tramp.texi (Default User): Fix typo.
(Cleanup remote connections): Adapt arguments of
`tramp-cleanup-connection'.
(Renaming remote files): New node.
(Frequently Asked Questions): New item "How to save files when a
remote host isn't reachable anymore?".
* etc/NEWS: Add `tramp-rename-files' and `tramp-rename-these-files'.
* lisp/net/tramp-cmds.el (tramp-default-rename-alist)
(tramp-confirm-rename-file-names): New defcustoms.
(tramp-rename-read-file-name-dir)
(tramp-rename-read-file-name-init): New defsubsts.
(tramp-default-rename-file, tramp-rename-files)
(tramp-rename-these-files): New defuns.
* lisp/net/tramp-integration.el (ido, ivy): Integrate with them.
Diffstat (limited to 'lisp')
| -rw-r--r-- | lisp/net/tramp-cmds.el | 267 | ||||
| -rw-r--r-- | lisp/net/tramp-integration.el | 16 |
2 files changed, 278 insertions, 5 deletions
diff --git a/lisp/net/tramp-cmds.el b/lisp/net/tramp-cmds.el index 56ccf738070..96b11c7f524 100644 --- a/lisp/net/tramp-cmds.el +++ b/lisp/net/tramp-cmds.el | |||
| @@ -195,6 +195,268 @@ This includes password cache, file cache, connection cache, buffers." | |||
| 195 | (dolist (name (tramp-list-remote-buffers)) | 195 | (dolist (name (tramp-list-remote-buffers)) |
| 196 | (when (bufferp (get-buffer name)) (kill-buffer name)))) | 196 | (when (bufferp (get-buffer name)) (kill-buffer name)))) |
| 197 | 197 | ||
| 198 | ;;;###tramp-autoload | ||
| 199 | (defcustom tramp-default-rename-alist nil | ||
| 200 | "Default target for renaming remote buffer file names. | ||
| 201 | This is an alist of cons cells (SOURCE . TARGET). The first | ||
| 202 | matching item specifies the target to be applied for renaming | ||
| 203 | buffer file names from source via `tramp-rename-files'. SOURCE | ||
| 204 | is a regular expressions, which matches a remote file name. | ||
| 205 | TARGET must be a directory name, which could be remote (including | ||
| 206 | remote directories Tramp infers by default, such as | ||
| 207 | \"/method:user@host:\"). | ||
| 208 | |||
| 209 | TARGET can contain the patterns %m, %u or %h, which are replaced | ||
| 210 | by the method name, user name or host name of SOURCE when calling | ||
| 211 | `tramp-rename-files'. | ||
| 212 | |||
| 213 | SOURCE could also be a Lisp form, which will be evaluated. The | ||
| 214 | result must be a string or nil, which is interpreted as a regular | ||
| 215 | expression which always matches." | ||
| 216 | :group 'tramp | ||
| 217 | :version "27.1" | ||
| 218 | :type '(repeat (cons (choice :tag "Source regexp" regexp sexp) | ||
| 219 | (choice :tag "Target name" string (const nil))))) | ||
| 220 | |||
| 221 | ;;;###tramp-autoload | ||
| 222 | (defcustom tramp-confirm-rename-file-names t | ||
| 223 | "Whether renaming a buffer file name must be confirmed." | ||
| 224 | :group 'tramp | ||
| 225 | :version "27.1" | ||
| 226 | :type 'boolean) | ||
| 227 | |||
| 228 | (defun tramp-default-rename-file (string) | ||
| 229 | "Determine default file name for renaming according to STRING. | ||
| 230 | The user option `tramp-default-rename-alist' is consulted, | ||
| 231 | finding the default mapping. If there is no matching entry, the | ||
| 232 | function returns nil" | ||
| 233 | (when (tramp-tramp-file-p string) | ||
| 234 | (let ((tdra tramp-default-rename-alist) | ||
| 235 | (method (or (file-remote-p string 'method) "")) | ||
| 236 | (user (or (file-remote-p string 'user) "")) | ||
| 237 | (host (or (file-remote-p string 'host) "")) | ||
| 238 | item result) | ||
| 239 | (while (setq item (pop tdra)) | ||
| 240 | (when (string-match-p (or (eval (car item)) "") string) | ||
| 241 | (setq tdra nil | ||
| 242 | result | ||
| 243 | (format-spec | ||
| 244 | (cdr item) (format-spec-make ?m method ?u user ?h host))))) | ||
| 245 | result))) | ||
| 246 | |||
| 247 | (defsubst tramp-rename-read-file-name-dir (string) | ||
| 248 | "Return the DIR entry to be applied in `read-file-name', based on STRING." | ||
| 249 | (when (tramp-tramp-file-p string) | ||
| 250 | (substring (file-remote-p string) 0 -1))) | ||
| 251 | |||
| 252 | (defsubst tramp-rename-read-file-name-init (string) | ||
| 253 | "Return the INIT entry to be applied in `read-file-name', based on STRING." | ||
| 254 | (when (tramp-tramp-file-p string) | ||
| 255 | (string-remove-prefix (tramp-rename-read-file-name-dir string) string))) | ||
| 256 | |||
| 257 | ;;;###tramp-autoload | ||
| 258 | (defun tramp-rename-files (source target) | ||
| 259 | "Replace in all buffers the visiting file name from SOURCE to TARGET. | ||
| 260 | SOURCE is a remote directory name, which could contain also a | ||
| 261 | localname part. TARGET is the directory name SOURCE is replaced | ||
| 262 | with. Often, TARGET is a remote directory name on another host, | ||
| 263 | but it can also be a local directory name. If TARGET has no | ||
| 264 | local part, the local part from SOURCE is used. | ||
| 265 | |||
| 266 | If TARGET is nil, it is selected according to the first match in | ||
| 267 | `tramp-default-rename-alist'. If called interactively, this | ||
| 268 | match is offered as initial value for selection. | ||
| 269 | |||
| 270 | On all buffers, which have a `buffer-file-name' matching SOURCE, | ||
| 271 | this name is modified by replacing SOURCE with TARGET. This is | ||
| 272 | applied by calling `set-visited-file-name'. The new | ||
| 273 | `buffer-file-name' is prompted for modification in the | ||
| 274 | minibuffer. The buffers are marked modified, and must be saved | ||
| 275 | explicitly. | ||
| 276 | |||
| 277 | If user option `tramp-confirm-rename-file-names' is nil, changing | ||
| 278 | the file name happens without confirmation. This requires a | ||
| 279 | matching entry in `tramp-default-rename-alist'. | ||
| 280 | |||
| 281 | Remote buffers related to the remote connection identified by | ||
| 282 | SOURCE, which are not visiting files, or which are visiting files | ||
| 283 | not matching SOURCE, are not modified. | ||
| 284 | |||
| 285 | Interactively, TARGET is selected from `tramp-default-rename-alist' | ||
| 286 | without confirmation if the prefix argument is non-nil. | ||
| 287 | |||
| 288 | The remote connection identified by SOURCE is flushed by | ||
| 289 | `tramp-cleanup-connection'." | ||
| 290 | (interactive | ||
| 291 | (let ((connections | ||
| 292 | (mapcar #'tramp-make-tramp-file-name (tramp-list-connections))) | ||
| 293 | ;; Completion packages do their voodoo in `completing-read' | ||
| 294 | ;; and `read-file-name', which is often incompatible with | ||
| 295 | ;; Tramp. Ignore them. | ||
| 296 | (completing-read-function #'completing-read-default) | ||
| 297 | (read-file-name-function #'read-file-name-default) | ||
| 298 | source target) | ||
| 299 | (if (null connections) | ||
| 300 | (tramp-user-error nil "There are no remote connections.") | ||
| 301 | (setq source | ||
| 302 | ;; Likely, the source remote connection is broken. So we | ||
| 303 | ;; shall avoid any action on it. | ||
| 304 | (let (non-essential) | ||
| 305 | (completing-read-default | ||
| 306 | "Enter old Tramp connection: " | ||
| 307 | ;; Completion function. | ||
| 308 | (completion-table-dynamic | ||
| 309 | (lambda (string) | ||
| 310 | (cond | ||
| 311 | ;; Initially, show existing remote connections. | ||
| 312 | ((not (tramp-tramp-file-p string)) | ||
| 313 | (all-completions string connections)) | ||
| 314 | ;; There is a selected remote connection. Show | ||
| 315 | ;; its longest common directory path of respective | ||
| 316 | ;; buffers. | ||
| 317 | (t (mapcar | ||
| 318 | (lambda (buffer) | ||
| 319 | (let ((bfn (buffer-file-name buffer))) | ||
| 320 | (and (buffer-live-p buffer) | ||
| 321 | (tramp-equal-remote string bfn) | ||
| 322 | (stringp bfn) (file-name-directory bfn)))) | ||
| 323 | (tramp-list-remote-buffers)))))) | ||
| 324 | #'tramp-tramp-file-p t | ||
| 325 | ;; If the current buffer is a remote one, it is likely | ||
| 326 | ;; that this connection is meant. So we offer it as | ||
| 327 | ;; initial value. Otherwise, use the longest remote | ||
| 328 | ;; connection path as initial value. | ||
| 329 | (or (file-remote-p default-directory) | ||
| 330 | (try-completion "" connections)))) | ||
| 331 | |||
| 332 | target | ||
| 333 | (when (null current-prefix-arg) | ||
| 334 | ;; The source remote connection shall not trigger any action. | ||
| 335 | ;; FIXME: Better error prompt when trying to access source host. | ||
| 336 | (let* ((default (or (tramp-default-rename-file source) source)) | ||
| 337 | (dir (tramp-rename-read-file-name-dir default)) | ||
| 338 | (init (tramp-rename-read-file-name-init default)) | ||
| 339 | (tramp-ignored-file-name-regexp | ||
| 340 | (regexp-quote (file-remote-p source)))) | ||
| 341 | (read-file-name-default | ||
| 342 | "Enter new Tramp connection: " | ||
| 343 | dir default 'confirm init #'file-directory-p))))) | ||
| 344 | |||
| 345 | (list source target))) | ||
| 346 | |||
| 347 | (unless (tramp-tramp-file-p source) | ||
| 348 | (tramp-user-error nil "Source %s must be remote." source)) | ||
| 349 | (when (null target) | ||
| 350 | (or (setq target (tramp-default-rename-file source)) | ||
| 351 | (tramp-user-error | ||
| 352 | nil | ||
| 353 | (eval-when-compile | ||
| 354 | (concat "There is no target specified. " | ||
| 355 | "Check `tramp-default-rename-alist' for a proper entry."))))) | ||
| 356 | (when (tramp-equal-remote source target) | ||
| 357 | (tramp-user-error nil "Source and target must have different remote.")) | ||
| 358 | |||
| 359 | ;; Append local file name if none is specified. | ||
| 360 | (when (string-equal (file-remote-p target) target) | ||
| 361 | (setq target (concat target (file-remote-p source 'localname)))) | ||
| 362 | ;; Make them directoy names. | ||
| 363 | (setq source (directory-file-name source) | ||
| 364 | target (directory-file-name target)) | ||
| 365 | |||
| 366 | ;; Rename visited file names of source buffers. | ||
| 367 | (save-window-excursion | ||
| 368 | (save-current-buffer | ||
| 369 | (let ((help-form "\ | ||
| 370 | Type SPC or `y' to set visited file name, | ||
| 371 | DEL or `n' to skip to next, | ||
| 372 | `e' to edit the visited file name, | ||
| 373 | ESC or `q' to quit without changing further buffers, | ||
| 374 | `!' to change all remaining buffers with no more questions.") | ||
| 375 | (query-choices '(?y ?\s ?n ?\177 ?! ?e ?q ?\e)) | ||
| 376 | (query (unless tramp-confirm-rename-file-names ?!)) | ||
| 377 | changed-buffers) | ||
| 378 | (dolist (buffer (tramp-list-remote-buffers)) | ||
| 379 | (switch-to-buffer buffer) | ||
| 380 | (let* ((bfn (buffer-file-name)) | ||
| 381 | (new-bfn (and (stringp bfn) | ||
| 382 | (replace-regexp-in-string | ||
| 383 | (regexp-quote source) target bfn))) | ||
| 384 | (prompt (format-message | ||
| 385 | "Set visited file name to `%s' [Type yn!eq or %s] " | ||
| 386 | new-bfn (key-description (vector help-char))))) | ||
| 387 | (when (and (buffer-live-p buffer) (stringp bfn) | ||
| 388 | (string-prefix-p source bfn) | ||
| 389 | ;; Skip, and don't ask again. | ||
| 390 | (not (memq query '(?q ?\e)))) | ||
| 391 | ;; Read prompt. | ||
| 392 | (unless (eq query ?!) | ||
| 393 | (setq query (read-char-choice prompt query-choices))) | ||
| 394 | ;; Edit the new buffer file name. | ||
| 395 | (when (eq query ?e) | ||
| 396 | (setq new-bfn | ||
| 397 | (read-file-name | ||
| 398 | "New visited file name: " | ||
| 399 | (file-name-directory new-bfn) new-bfn))) | ||
| 400 | ;; Set buffer file name. Remember the change. | ||
| 401 | (when (memq query '(?y ?\s ?! ?e)) | ||
| 402 | (setq changed-buffers | ||
| 403 | (cons (list buffer bfn (buffer-modified-p)) | ||
| 404 | changed-buffers)) | ||
| 405 | (set-visited-file-name new-bfn)) | ||
| 406 | ;; Quit. Revert changes if prompted by user. | ||
| 407 | (when (and (memq query '(?q ?\e)) changed-buffers | ||
| 408 | (y-or-n-p "Do you want to revert applied changes?")) | ||
| 409 | (dolist (item changed-buffers) | ||
| 410 | (with-current-buffer (car item) | ||
| 411 | (set-visited-file-name (nth 1 item)) | ||
| 412 | (set-buffer-modified-p (nth 2 item))))) | ||
| 413 | ;; Cleanup echo area. | ||
| 414 | (message nil))))))) | ||
| 415 | |||
| 416 | ;; Cleanup. | ||
| 417 | (tramp-cleanup-connection (tramp-dissect-file-name source))) | ||
| 418 | |||
| 419 | ;;;###tramp-autoload | ||
| 420 | (defun tramp-rename-these-files (target) | ||
| 421 | "Replace visiting file names to TARGET. | ||
| 422 | The current buffer must be related to a remote connection. In | ||
| 423 | all buffers, which are visiting a file with the same directory | ||
| 424 | name, the buffer file name is changed. | ||
| 425 | |||
| 426 | Interactively, TARGET is selected from `tramp-default-rename-alist' | ||
| 427 | without confirmation if the prefix argument is non-nil. | ||
| 428 | |||
| 429 | For details, see `tramp-rename-files'." | ||
| 430 | (interactive | ||
| 431 | (let ((source default-directory) | ||
| 432 | target | ||
| 433 | ;; Completion packages do their voodoo in `completing-read' | ||
| 434 | ;; and `read-file-name', which is often incompatible with | ||
| 435 | ;; Tramp. Ignore them. | ||
| 436 | (completing-read-function #'completing-read-default) | ||
| 437 | (read-file-name-function #'read-file-name-default)) | ||
| 438 | (if (not (tramp-tramp-file-p source)) | ||
| 439 | (tramp-user-error | ||
| 440 | nil | ||
| 441 | (substitute-command-keys | ||
| 442 | (concat "Current buffer is not remote. " | ||
| 443 | "Consider `\\[tramp-rename-files]' instead."))) | ||
| 444 | (setq target | ||
| 445 | (when (null current-prefix-arg) | ||
| 446 | ;; The source remote connection shall not trigger any action. | ||
| 447 | ;; FIXME: Better error prompt when trying to access source host. | ||
| 448 | (let* ((default (or (tramp-default-rename-file source) source)) | ||
| 449 | (dir (tramp-rename-read-file-name-dir default)) | ||
| 450 | (init (tramp-rename-read-file-name-init default)) | ||
| 451 | (tramp-ignored-file-name-regexp | ||
| 452 | (regexp-quote (file-remote-p source)))) | ||
| 453 | (read-file-name-default | ||
| 454 | (format "Change Tramp connection `%s': " source) | ||
| 455 | dir default 'confirm init #'file-directory-p))))) | ||
| 456 | (list target))) | ||
| 457 | |||
| 458 | (tramp-rename-files default-directory target)) | ||
| 459 | |||
| 198 | ;; Tramp version is useful in a number of situations. | 460 | ;; Tramp version is useful in a number of situations. |
| 199 | 461 | ||
| 200 | ;;;###tramp-autoload | 462 | ;;;###tramp-autoload |
| @@ -424,11 +686,6 @@ please ensure that the buffers are attached to your email.\n\n")) | |||
| 424 | 686 | ||
| 425 | ;; * Clean up unused *tramp/foo* buffers after a while. (Pete Forman) | 687 | ;; * Clean up unused *tramp/foo* buffers after a while. (Pete Forman) |
| 426 | ;; | 688 | ;; |
| 427 | ;; * WIBNI there was an interactive command prompting for Tramp | ||
| 428 | ;; method, hostname, username and filename and translates the user | ||
| 429 | ;; input into the correct filename syntax (depending on the Emacs | ||
| 430 | ;; flavor) (Reiner Steib) | ||
| 431 | ;; | ||
| 432 | ;; * Let the user edit the connection properties interactively. | 689 | ;; * Let the user edit the connection properties interactively. |
| 433 | ;; Something like `gnus-server-edit-server' in Gnus' *Server* buffer. | 690 | ;; Something like `gnus-server-edit-server' in Gnus' *Server* buffer. |
| 434 | 691 | ||
diff --git a/lisp/net/tramp-integration.el b/lisp/net/tramp-integration.el index 0bb19ed9c4d..0c3107603fb 100644 --- a/lisp/net/tramp-integration.el +++ b/lisp/net/tramp-integration.el | |||
| @@ -36,6 +36,8 @@ | |||
| 36 | (declare-function tramp-file-name-equal-p "tramp") | 36 | (declare-function tramp-file-name-equal-p "tramp") |
| 37 | (declare-function tramp-tramp-file-p "tramp") | 37 | (declare-function tramp-tramp-file-p "tramp") |
| 38 | (defvar eshell-path-env) | 38 | (defvar eshell-path-env) |
| 39 | (defvar ido-read-file-name-non-ido) | ||
| 40 | (defvar ivy-completing-read-handlers-alist) | ||
| 39 | (defvar recentf-exclude) | 41 | (defvar recentf-exclude) |
| 40 | (defvar tramp-current-connection) | 42 | (defvar tramp-current-connection) |
| 41 | (defvar tramp-postfix-host-format) | 43 | (defvar tramp-postfix-host-format) |
| @@ -170,6 +172,20 @@ NAME must be equal to `tramp-current-connection'." | |||
| 170 | (remove-hook 'tramp-cleanup-all-connections-hook | 172 | (remove-hook 'tramp-cleanup-all-connections-hook |
| 171 | #'tramp-recentf-cleanup-all)))) | 173 | #'tramp-recentf-cleanup-all)))) |
| 172 | 174 | ||
| 175 | ;;; Integration of ido.el: | ||
| 176 | |||
| 177 | (with-eval-after-load 'ido | ||
| 178 | (add-to-list 'ido-read-file-name-non-ido 'tramp-rename-files) | ||
| 179 | (add-to-list 'ido-read-file-name-non-ido 'tramp-these-rename-files)) | ||
| 180 | |||
| 181 | ;;; Integration of ivy.el: | ||
| 182 | |||
| 183 | (with-eval-after-load 'ivy | ||
| 184 | (add-to-list 'ivy-completing-read-handlers-alist | ||
| 185 | '(tramp-rename-files . completing-read-default)) | ||
| 186 | (add-to-list 'ivy-completing-read-handlers-alist | ||
| 187 | '(tramp-these-rename-files . completing-read-default))) | ||
| 188 | |||
| 173 | ;;; Default connection-local variables for Tramp: | 189 | ;;; Default connection-local variables for Tramp: |
| 174 | 190 | ||
| 175 | (defconst tramp-connection-local-default-profile | 191 | (defconst tramp-connection-local-default-profile |