wdired.el 31.7 KB
Newer Older
Stefan Monnier's avatar
Stefan Monnier committed
1 2
;;; wdired.el --- Rename files editing their names in dired buffers

3
;; Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011
4
;;   Free Software Foundation, Inc.
Stefan Monnier's avatar
Stefan Monnier committed
5 6

;; Filename: wdired.el
7 8
;; Author: Juan León Lahoz García <juanleon1@gmail.com>
;; Version: 2.0
Stefan Monnier's avatar
Stefan Monnier committed
9 10 11 12
;; Keywords: dired, environment, files, renaming

;; This file is part of GNU Emacs.

13 14 15 16
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
Stefan Monnier's avatar
Stefan Monnier committed
17

18 19 20 21
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
Stefan Monnier's avatar
Stefan Monnier committed
22 23

;; You should have received a copy of the GNU General Public License
24
;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
Stefan Monnier's avatar
Stefan Monnier committed
25 26 27 28 29 30 31

;;; Commentary:

;; wdired.el (the "w" is for writable) provides an alternative way of
;; renaming files.
;;
;; Have you ever wished to use C-x r t (string-rectangle), M-%
Stefan Monnier's avatar
Stefan Monnier committed
32
;; (query-replace), M-c (capitalize-word), etc... to change the name of
Stefan Monnier's avatar
Stefan Monnier committed
33 34
;; the files in a "dired" buffer? Now you can do this.  All the power
;; of Emacs commands are available to renaming files!
Stefan Monnier's avatar
Stefan Monnier committed
35
;;
Stefan Monnier's avatar
Stefan Monnier committed
36 37 38 39 40 41
;; This package provides a function that makes the filenames of a a
;; dired buffer editable, by changing the buffer mode (which inhibits
;; all of the commands of dired mode). Here you can edit the names of
;; one or more files and directories, and when you press C-c C-c, the
;; renaming takes effect and you are back to dired mode.
;;
Stefan Monnier's avatar
Stefan Monnier committed
42
;; Another things you can do with WDired:
Stefan Monnier's avatar
Stefan Monnier committed
43 44 45 46 47 48 49 50
;;
;; - To move files to another directory (by typing their path,
;;   absolute or relative, as a part of the new filename).
;;
;; - To change the target of symbolic links.
;;
;; - To change the permission bits of the filenames (in systems with a
;;   working unix-alike `dired-chmod-program'). See and customize the
Stefan Monnier's avatar
Stefan Monnier committed
51
;;   variable `wdired-allow-to-change-permissions'.  To change a single
Stefan Monnier's avatar
Stefan Monnier committed
52
;;   char (toggling between its two more usual values) you can press
Stefan Monnier's avatar
Stefan Monnier committed
53
;;   the space bar over it or left-click the mouse.  To set any char to
Stefan Monnier's avatar
Stefan Monnier committed
54
;;   an specific value (this includes the SUID, SGID and STI bits) you
Stefan Monnier's avatar
Stefan Monnier committed
55
;;   can use the key labeled as the letter you want.  Please note that
Stefan Monnier's avatar
Stefan Monnier committed
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
;;   permissions of the links cannot be changed in that way, because
;;   the change would affect to their targets, and this would not be
;;   WYSIWYG :-).
;;
;; - To mark files for deletion, by deleting their whole filename.

;;; Installation:

;; Add this file (byte-compiling it is recommended) to your load-path.
;; Then add one of these set of lines (or similar ones) to your config:
;;
;; This is the easy way:
;;
;; (require 'wdired)
;; (define-key dired-mode-map "r" 'wdired-change-to-wdired-mode)
;;
Stefan Monnier's avatar
Stefan Monnier committed
72 73
;; This is the recommended way for faster Emacs startup time and lower
;; memory consumption:
Stefan Monnier's avatar
Stefan Monnier committed
74 75
;;
;; (autoload 'wdired-change-to-wdired-mode "wdired")
Stefan Monnier's avatar
Stefan Monnier committed
76
;; (eval-after-load "dired"
Stefan Monnier's avatar
Stefan Monnier committed
77 78 79 80 81 82
;;           '(lambda ()
;;              (define-key dired-mode-map "r" 'wdired-change-to-wdired-mode)
;;              (define-key dired-mode-map
;;                [menu-bar immediate wdired-change-to-wdired-mode]
;;                '("Edit File Names" . wdired-change-to-wdired-mode))))
;;
Stefan Monnier's avatar
Stefan Monnier committed
83 84
;; Type "M-x customize-group RET wdired" if you want to make changes
;; to the default behavior.
Stefan Monnier's avatar
Stefan Monnier committed
85 86 87 88 89 90 91 92 93 94 95 96 97

;;; Usage:

;; Then, you can start editing the names of the files by typing "r"
;; (or whatever key you choose, or M-x wdired-change-to-wdired-mode).
;; Use C-c C-c when finished or C-c C-k to abort. You can use also the
;; menu options: in dired mode, "Edit File Names" under "Immediate".
;; While editing the names, a new submenu "WDired" is available at top
;; level. You can customize the behavior of this package from this
;; menu.

;;; Change Log:

Stefan Monnier's avatar
Stefan Monnier committed
98 99
;; Google is your friend (previous versions with complete changelogs
;; were posted to gnu.emacs.sources)
Stefan Monnier's avatar
Stefan Monnier committed
100 101 102

;;; Code:

Stefan Monnier's avatar
Stefan Monnier committed
103
(defvar dired-backup-overwrite) ; Only in Emacs 20.x this is a custom var
Stefan Monnier's avatar
Stefan Monnier committed
104

105
(eval-when-compile (require 'cl))
Stefan Monnier's avatar
Stefan Monnier committed
106 107
(require 'dired)
(autoload 'dired-do-create-files-regexp "dired-aux")
Stefan Monnier's avatar
Stefan Monnier committed
108 109 110 111 112 113

(defgroup wdired nil
  "Mode to rename files by editing their names in dired buffers."
  :group 'dired)

(defcustom wdired-use-interactive-rename nil
Stefan Monnier's avatar
Stefan Monnier committed
114
  "If non-nil, WDired requires confirmation before actually renaming files.
Stefan Monnier's avatar
Stefan Monnier committed
115 116 117
If nil, WDired doesn't require confirmation to change the file names,
and the variable `wdired-confirm-overwrite' controls whether it is ok
to overwrite files without asking."
Stefan Monnier's avatar
Stefan Monnier committed
118 119 120
  :type 'boolean
  :group 'wdired)

Stefan Monnier's avatar
Stefan Monnier committed
121
(defcustom wdired-confirm-overwrite t
Stefan Monnier's avatar
Stefan Monnier committed
122
  "If nil the renames can overwrite files without asking.
Stefan Monnier's avatar
Stefan Monnier committed
123 124
This variable has no effect at all if `wdired-use-interactive-rename'
is not nil."
Stefan Monnier's avatar
Stefan Monnier committed
125 126 127
  :type 'boolean
  :group 'wdired)

Stefan Monnier's avatar
Stefan Monnier committed
128
(defcustom wdired-use-dired-vertical-movement nil
Stefan Monnier's avatar
Stefan Monnier committed
129
  "If t, the \"up\" and \"down\" movement works as in Dired mode.
Stefan Monnier's avatar
Stefan Monnier committed
130 131
That is, always move the point to the beginning of the filename at line.

132 133
If `sometimes', only move to the beginning of filename if the point is
before it, and `track-eol' is non-nil.  This behavior is very handy
Stefan Monnier's avatar
Stefan Monnier committed
134 135 136 137 138 139 140 141 142
when editing several filenames.

If nil, \"up\" and \"down\" movement is done as in any other buffer."
  :type '(choice (const :tag "As in any other mode" nil)
		 (const :tag "Smart cursor placement" sometimes)
		 (other :tag "As in dired mode" t))
  :group 'wdired)

(defcustom wdired-allow-to-redirect-links t
Stefan Monnier's avatar
Stefan Monnier committed
143
  "If non-nil, the target of the symbolic links are editable.
Stefan Monnier's avatar
Stefan Monnier committed
144 145 146 147 148 149
In systems without symbolic links support, this variable has no effect
at all."
  :type 'boolean
  :group 'wdired)

(defcustom wdired-allow-to-change-permissions nil
Stefan Monnier's avatar
Stefan Monnier committed
150
  "If non-nil, the permissions bits of the files are editable.
Stefan Monnier's avatar
Stefan Monnier committed
151 152 153 154 155

If t, to change a single bit, put the cursor over it and press the
space bar, or left click over it.  You can also hit the letter you want
to set: if this value is allowed, the character in the buffer will be
changed.  Anyway, the point is advanced one position, so, for example,
Stefan Monnier's avatar
Stefan Monnier committed
156
you can keep the <x> key pressed to give execution permissions to
Stefan Monnier's avatar
Stefan Monnier committed
157 158
everybody to that file.

159
If `advanced', the bits are freely editable.  You can use
Stefan Monnier's avatar
Stefan Monnier committed
160 161 162 163
`string-rectangle', `query-replace', etc.  You can put any value (even
newlines), but if you want your changes to be useful, you better put a
intelligible value.

Stefan Monnier's avatar
Stefan Monnier committed
164
Anyway, the real change of the permissions is done by the external
Stefan Monnier's avatar
Stefan Monnier committed
165 166
program `dired-chmod-program', which must exist."
  :type '(choice (const :tag "Not allowed" nil)
167
                 (const :tag "Toggle/set bits" t)
Stefan Monnier's avatar
Stefan Monnier committed
168 169 170
		 (other :tag "Bits freely editable" advanced))
  :group 'wdired)

171 172 173 174 175 176
(defvar wdired-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map "\C-x\C-s" 'wdired-finish-edit)
    (define-key map "\C-c\C-c" 'wdired-finish-edit)
    (define-key map "\C-c\C-k" 'wdired-abort-changes)
    (define-key map "\C-c\C-[" 'wdired-abort-changes)
Juri Linkov's avatar
Juri Linkov committed
177
    (define-key map "\C-x\C-q" 'wdired-exit)
Stefan Monnier's avatar
Stefan Monnier committed
178 179 180
    (define-key map "\C-m"     'ignore)
    (define-key map "\C-j"     'ignore)
    (define-key map "\C-o"     'ignore)
181 182 183 184 185 186 187 188 189 190
    (define-key map [up]       'wdired-previous-line)
    (define-key map "\C-p"     'wdired-previous-line)
    (define-key map [down]     'wdired-next-line)
    (define-key map "\C-n"     'wdired-next-line)

    (define-key map [menu-bar wdired]
      (cons "WDired" (make-sparse-keymap "WDired")))
    (define-key map [menu-bar wdired wdired-customize]
      '("Options" . wdired-customize))
    (define-key map [menu-bar wdired dashes]
Stefan Monnier's avatar
Stefan Monnier committed
191
      '("--"))
192
    (define-key map [menu-bar wdired wdired-abort-changes]
193 194
      '(menu-item "Abort Changes" wdired-abort-changes
		  :help "Abort changes and return to dired mode"))
195 196
    (define-key map [menu-bar wdired wdired-finish-edit]
      '("Commit Changes" . wdired-finish-edit))
Stefan Monnier's avatar
Stefan Monnier committed
197 198 199 200 201

    (define-key map [remap upcase-word] 'wdired-upcase-word)
    (define-key map [remap capitalize-word] 'wdired-capitalize-word)
    (define-key map [remap downcase-word] 'wdired-downcase-word)

202 203 204
    map))

(defvar wdired-mode-hook nil
Stefan Monnier's avatar
Stefan Monnier committed
205
  "Hooks run when changing to WDired mode.")
Stefan Monnier's avatar
Stefan Monnier committed
206 207 208 209

;; Local variables (put here to avoid compilation gripes)
(defvar wdired-col-perm) ;; Column where the permission bits start
(defvar wdired-old-content)
210
(defvar wdired-old-point)
Stefan Monnier's avatar
Stefan Monnier committed
211 212 213 214 215


(defun wdired-mode ()
  "\\<wdired-mode-map>File Names Editing mode.

Stefan Monnier's avatar
Stefan Monnier committed
216 217
Press \\[wdired-finish-edit] to make the changes to take effect
and exit.  To abort the edit, use \\[wdired-abort-changes].
Stefan Monnier's avatar
Stefan Monnier committed
218

Stefan Monnier's avatar
Stefan Monnier committed
219 220 221
In this mode you can edit the names of the files, the target of
the links and the permission bits of the files.  You can use
\\[customize-group] RET wdired to customize WDired behavior.
Stefan Monnier's avatar
Stefan Monnier committed
222

Stefan Monnier's avatar
Stefan Monnier committed
223 224
The only editable texts in a WDired buffer are filenames,
symbolic link targets, and filenames permission."
Stefan Monnier's avatar
Stefan Monnier committed
225 226 227 228 229 230 231 232
  (interactive)
  (error "This mode can be enabled only by `wdired-change-to-wdired-mode'"))
(put 'wdired-mode 'mode-class 'special)


;;;###autoload
(defun wdired-change-to-wdired-mode ()
  "Put a dired buffer in a mode in which filenames are editable.
Stefan Monnier's avatar
Stefan Monnier committed
233 234 235 236
\\<wdired-mode-map>
This mode allows the user to change the names of the files, and after
typing \\[wdired-finish-edit] Emacs renames the files and directories
in disk.
Stefan Monnier's avatar
Stefan Monnier committed
237 238 239

See `wdired-mode'."
  (interactive)
240 241
  (or (eq major-mode 'dired-mode)
      (error "Not a Dired buffer"))
Stefan Monnier's avatar
Stefan Monnier committed
242 243
  (set (make-local-variable 'wdired-old-content)
       (buffer-substring (point-min) (point-max)))
244
  (set (make-local-variable 'wdired-old-point) (point))
245
  (set (make-local-variable 'query-replace-skip-read-only) t)
Stefan Monnier's avatar
Stefan Monnier committed
246
  (use-local-map wdired-mode-map)
247
  (force-mode-line-update)
Stefan Monnier's avatar
Stefan Monnier committed
248 249 250 251
  (setq buffer-read-only nil)
  (dired-unadvertise default-directory)
  (add-hook 'kill-buffer-hook 'wdired-check-kill-buffer nil t)
  (setq major-mode 'wdired-mode)
Stefan Monnier's avatar
Stefan Monnier committed
252
  (setq mode-name "Editable Dired")
Stefan Monnier's avatar
Stefan Monnier committed
253 254 255 256 257 258 259 260 261 262 263 264 265
  (setq revert-buffer-function 'wdired-revert)
  ;; I temp disable undo for performance: since I'm going to clear the
  ;; undo list, it can save more than a 9% of time with big
  ;; directories because setting properties modify the undo-list.
  (buffer-disable-undo)
  (wdired-preprocess-files)
  (if wdired-allow-to-change-permissions
      (wdired-preprocess-perms))
  (if (and wdired-allow-to-redirect-links (fboundp 'make-symbolic-link))
      (wdired-preprocess-symlinks))
  (buffer-enable-undo) ; Performance hack. See above.
  (set-buffer-modified-p nil)
  (setq buffer-undo-list nil)
266
  (run-mode-hooks 'wdired-mode-hook)
267 268
  (message "%s" (substitute-command-keys
		 "Press \\[wdired-finish-edit] when finished \
Juri Linkov's avatar
Juri Linkov committed
269
or \\[wdired-abort-changes] to abort changes")))
Stefan Monnier's avatar
Stefan Monnier committed
270 271 272 273 274


;; Protect the buffer so only the filenames can be changed, and put
;; properties so filenames (old and new) can be easily found.
(defun wdired-preprocess-files ()
275
  (put-text-property (point-min) (1+ (point-min))'front-sticky t)
Stefan Monnier's avatar
Stefan Monnier committed
276 277 278 279 280 281
  (save-excursion
    (goto-char (point-min))
    (let ((b-protection (point))
	  filename)
      (while (not (eobp))
	(setq filename (dired-get-filename nil t))
282 283 284
        (when (and filename
		   (not (member (file-name-nondirectory filename) '("." ".."))))
	  (dired-move-to-filename)
285 286 287 288 289 290 291
	  ;; The rear-nonsticky property below shall ensure that text preceding
	  ;; the filename can't be modified.
	  (add-text-properties
	   (1- (point)) (point) `(old-name ,filename rear-nonsticky (read-only)))
	  (put-text-property b-protection (point) 'read-only t)
	  (setq b-protection (dired-move-to-end-of-filename t))
	  (put-text-property (point) (1+ (point)) 'end-name t))
Stefan Monnier's avatar
Stefan Monnier committed
292 293 294 295 296 297
        (forward-line))
      (put-text-property b-protection (point-max) 'read-only t))))

;; This code is a copy of some dired-get-filename lines.
(defsubst wdired-normalize-filename (file)
  (setq file
298 299
	;; FIXME: shouldn't we check for a `b' argument or somesuch before
	;; doing such unquoting?  --Stef
Stefan Monnier's avatar
Stefan Monnier committed
300
	(read (concat
301 302
	       "\"" (replace-regexp-in-string
		     "\\([^\\]\\|\\`\\)\"" "\\1\\\\\"" file)
Stefan Monnier's avatar
Stefan Monnier committed
303 304 305 306 307 308 309 310 311
	       "\"")))
  (and file buffer-file-coding-system
       (not file-name-coding-system)
       (not default-file-name-coding-system)
       (setq file (encode-coding-string file buffer-file-coding-system)))
  file)

(defun wdired-get-filename (&optional no-dir old)
  "Return the filename at line.
312
Similar to `dired-get-filename' but it doesn't rely on regexps.  It
Stefan Monnier's avatar
Stefan Monnier committed
313
relies on WDired buffer's properties.  Optional arg NO-DIR with value
Stefan Monnier's avatar
Stefan Monnier committed
314 315
non-nil means don't include directory.  Optional arg OLD with value
non-nil means return old filename."
316
  ;; FIXME: Use dired-get-filename's new properties.
317 318 319 320 321 322 323 324
  (let (beg end file)
    (save-excursion
      (setq end (line-end-position))
      (beginning-of-line)
      (setq beg (next-single-property-change (point) 'old-name nil end))
      (unless (eq beg end)
	(if old
	    (setq file (get-text-property beg 'old-name))
325 326 327 328 329
	  ;; In the following form changed `(1+ beg)' to `beg' so that
	  ;; the filename end is found even when the filename is empty.
	  ;; Fixes error and spurious newlines when marking files for
	  ;; deletion.
	  (setq end (next-single-property-change beg 'end-name))
330 331 332 333 334 335
	  (setq file (buffer-substring-no-properties (1+ beg) end)))
	(and file (setq file (wdired-normalize-filename file))))
      (if (or no-dir old)
	  file
	(and file (> (length file) 0)
             (concat (dired-current-directory) file))))))
Stefan Monnier's avatar
Stefan Monnier committed
336 337 338 339


(defun wdired-change-to-dired-mode ()
  "Change the mode back to dired."
340 341
  (or (eq major-mode 'wdired-mode)
      (error "Not a Wdired buffer"))
Stefan Monnier's avatar
Stefan Monnier committed
342
  (let ((inhibit-read-only t))
343 344 345
    (remove-text-properties
     (point-min) (point-max)
     '(front-sticky nil rear-nonsticky nil read-only nil keymap nil)))
Stefan Monnier's avatar
Stefan Monnier committed
346
  (use-local-map dired-mode-map)
347
  (force-mode-line-update)
Stefan Monnier's avatar
Stefan Monnier committed
348 349 350 351 352
  (setq buffer-read-only t)
  (setq major-mode 'dired-mode)
  (setq mode-name "Dired")
  (dired-advertise)
  (remove-hook 'kill-buffer-hook 'wdired-check-kill-buffer t)
Stefan Monnier's avatar
Stefan Monnier committed
353
  (set (make-local-variable 'revert-buffer-function) 'dired-revert))
Stefan Monnier's avatar
Stefan Monnier committed
354 355 356 357 358 359 360


(defun wdired-abort-changes ()
  "Abort changes and return to dired mode."
  (interactive)
  (let ((inhibit-read-only t))
    (erase-buffer)
361 362
    (insert wdired-old-content)
    (goto-char wdired-old-point))
Stefan Monnier's avatar
Stefan Monnier committed
363 364
  (wdired-change-to-dired-mode)
  (set-buffer-modified-p nil)
365 366
  (setq buffer-undo-list nil)
  (message "Changes aborted"))
Stefan Monnier's avatar
Stefan Monnier committed
367 368 369 370 371

(defun wdired-finish-edit ()
  "Actually rename files based on your editing in the Dired buffer."
  (interactive)
  (wdired-change-to-dired-mode)
372
  (let ((changes nil)
Stefan Monnier's avatar
Stefan Monnier committed
373
	(errors 0)
374 375 376 377
	files-deleted
	files-renamed
	some-file-names-unchanged
	file-old file-new tmp-value)
Stefan Monnier's avatar
Stefan Monnier committed
378
    (save-excursion
379 380 381 382 383 384 385 386 387 388
      (when (and wdired-allow-to-redirect-links
		 (fboundp 'make-symbolic-link))
	(setq tmp-value (wdired-do-symlink-changes))
	(setq errors (cdr tmp-value))
	(setq changes (car tmp-value)))
      (when (and wdired-allow-to-change-permissions
		 (boundp 'wdired-col-perm)) ; could have been changed
	(setq tmp-value (wdired-do-perm-changes))
	(setq errors (+ errors (cdr tmp-value)))
	(setq changes (or changes (car tmp-value))))
Stefan Monnier's avatar
Stefan Monnier committed
389 390
      (goto-char (point-max))
      (while (not (bobp))
391 392
	(setq file-old (wdired-get-filename nil t))
	(when file-old
393
	  (setq file-new (wdired-get-filename))
394 395
          (if (equal file-new file-old)
	      (setq some-file-names-unchanged t)
396 397
            (setq changes t)
            (if (not file-new)		;empty filename!
398 399 400
                (push file-old files-deleted)
              (push (cons file-old (substitute-in-file-name file-new))
                    files-renamed))))
Stefan Monnier's avatar
Stefan Monnier committed
401
	(forward-line -1)))
402 403
    (when files-renamed
      (setq errors (+ errors (wdired-do-renames files-renamed))))
Stefan Monnier's avatar
Stefan Monnier committed
404
    (if changes
405 406 407 408 409 410 411 412 413 414 415 416
	(progn
	  ;; If we are displaying a single file (rather than the
	  ;; contents of a directory), change dired-directory if that
	  ;; file was renamed.  (This ought to be generalized to
	  ;; handle the multiple files case, but that's less trivial).
	  (when (and (stringp dired-directory)
		     (not (file-directory-p dired-directory))
		     (null some-file-names-unchanged)
		     (= (length files-renamed) 1))
	    (setq dired-directory (cdr (car files-renamed))))
	  ;; Re-sort the buffer.
	  (revert-buffer))
Stefan Monnier's avatar
Stefan Monnier committed
417
      (let ((inhibit-read-only t))
Stefan Monnier's avatar
Stefan Monnier committed
418 419 420 421 422
	(remove-text-properties (point-min) (point-max)
				'(old-name nil end-name nil old-link nil
					   end-link nil end-perm nil
					   old-perm nil perm-changed nil))
	(message "(No changes to be performed)")))
423 424 425 426
    (when files-deleted
      (wdired-flag-for-deletion files-deleted))
    (when (> errors 0)
      (dired-log-summary (format "%d rename actions failed" errors) nil)))
Stefan Monnier's avatar
Stefan Monnier committed
427 428 429
  (set-buffer-modified-p nil)
  (setq buffer-undo-list nil))

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
(defun wdired-do-renames (renames)
  "Perform RENAMES in parallel."
  (let ((residue ())
        (progress nil)
        (errors 0)
        (overwrite (or (not wdired-confirm-overwrite) 1)))
    (while (or renames
               ;; We've done one round through the renames, we have found
               ;; some residue, but we also made some progress, so maybe
               ;; some of the residue were resolved: try again.
               (prog1 (setq renames residue)
                 (setq progress nil)
                 (setq residue nil)))
      (let* ((rename (pop renames))
             (file-new (cdr rename)))
        (cond
         ((rassoc file-new renames)
          (error "Trying to rename 2 files to the same name"))
         ((assoc file-new renames)
          ;; Renaming to a file name that already exists but will itself be
          ;; renamed as well.  Let's wait until that one gets renamed.
          (push rename residue))
         ((and (assoc file-new residue)
               ;; Make sure the file really exists: if it doesn't it's
               ;; not really a conflict.  It might be a temp-file generated
               ;; specifically to break a circular renaming.
               (file-exists-p file-new))
          ;; Renaming to a file name that already exists, needed to be renamed,
          ;; but whose renaming could not be performed right away.
          (if (or progress renames)
              ;; There's still a chance the conflict will be resolved.
              (push rename residue)
            ;; We have not made any progress and we've reached the end of
            ;; the renames, so we really have a circular conflict, and we
            ;; have to forcefully break the cycle.
            (message "Circular renaming: using temporary file name")
            (let ((tmp (make-temp-name file-new)))
              (push (cons (car rename) tmp) renames)
              (push (cons tmp file-new) residue))))
         (t
          (setq progress t)
          (let ((file-ori (car rename)))
            (if wdired-use-interactive-rename
                (wdired-search-and-rename file-ori file-new)
              ;; If dired-rename-file autoloads dired-aux while
              ;; dired-backup-overwrite is locally bound,
              ;; dired-backup-overwrite won't be initialized.
              ;; So we must ensure dired-aux is loaded.
              (require 'dired-aux)
              (condition-case err
                  (let ((dired-backup-overwrite nil))
                    (dired-rename-file file-ori file-new
                                       overwrite))
                (error
                 (setq errors (1+ errors))
                 (dired-log (concat "Rename `" file-ori "' to `"
                                    file-new "' failed:\n%s\n")
                            err)))))))))
    errors))
Juanma Barranquero's avatar
Juanma Barranquero committed
489

490

Juri Linkov's avatar
Juri Linkov committed
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
(defun wdired-exit ()
  "Exit wdired and return to dired mode.
Just return to dired mode if there are no changes.  Otherwise,
ask a yes-or-no question whether to save or cancel changes,
and proceed depending on the answer."
  (interactive)
  (if (buffer-modified-p)
      (if (y-or-n-p (format "Buffer %s modified; save changes? "
			    (current-buffer)))
	  (wdired-finish-edit)
	(wdired-abort-changes))
    (wdired-change-to-dired-mode)
    (set-buffer-modified-p nil)
    (setq buffer-undo-list nil)
    (message "(No changes need to be saved)")))

Stefan Monnier's avatar
Stefan Monnier committed
507
;; Rename a file, searching it in a modified dired buffer, in order
Stefan Monnier's avatar
Stefan Monnier committed
508
;; to be able to use `dired-do-create-files-regexp' and get its
Stefan Monnier's avatar
Stefan Monnier committed
509
;; "benefits".
Stefan Monnier's avatar
Stefan Monnier committed
510 511 512 513
(defun wdired-search-and-rename (filename-ori filename-new)
  (save-excursion
    (goto-char (point-max))
    (forward-line -1)
514
    (let ((done nil)
Stefan Monnier's avatar
Stefan Monnier committed
515
	  curr-filename)
516 517 518
      (while (and (not done) (not (bobp)))
        (setq curr-filename (wdired-get-filename nil t))
        (if (equal curr-filename filename-ori)
Stefan Monnier's avatar
Stefan Monnier committed
519
            (progn
520
              (setq done t)
Stefan Monnier's avatar
Stefan Monnier committed
521 522 523 524 525 526 527
              (let ((inhibit-read-only t))
                (dired-move-to-filename)
                (search-forward (wdired-get-filename t) nil t)
                (replace-match (file-name-nondirectory filename-ori) t t))
              (dired-do-create-files-regexp
               (function dired-rename-file)
               "Move" 1 ".*" filename-new nil t))
528
	  (forward-line -1))))))
Stefan Monnier's avatar
Stefan Monnier committed
529 530 531 532 533 534 535 536 537 538 539

;; marks a list of files for deletion
(defun wdired-flag-for-deletion (filenames-ori)
  (save-excursion
    (goto-char (point-min))
    (while (not (eobp))
      (if (member (dired-get-filename nil t) filenames-ori)
          (dired-flag-file-deletion 1)
	(forward-line)))))

(defun wdired-customize ()
Stefan Monnier's avatar
Stefan Monnier committed
540
  "Customize WDired options."
Stefan Monnier's avatar
Stefan Monnier committed
541 542 543 544
  (interactive)
  (customize-apropos "wdired" 'groups))

(defun wdired-revert (&optional arg noconfirm)
Stefan Monnier's avatar
Stefan Monnier committed
545 546
  "Discard changes in the buffer and update it based on changes on disk.
Optional arguments are ignored."
Stefan Monnier's avatar
Stefan Monnier committed
547 548 549 550 551
  (wdired-change-to-dired-mode)
  (revert-buffer)
  (wdired-change-to-wdired-mode))

(defun wdired-check-kill-buffer ()
552
  ;; FIXME: Can't we use the normal mechanism for that?  --Stef
Stefan Monnier's avatar
Stefan Monnier committed
553 554 555
  (if (and
       (buffer-modified-p)
       (not (y-or-n-p "Buffer changed. Discard changes and kill buffer? ")))
Juanma Barranquero's avatar
Juanma Barranquero committed
556
      (error "Error")))
Stefan Monnier's avatar
Stefan Monnier committed
557 558 559

(defun wdired-next-line (arg)
  "Move down lines then position at filename or the current column.
Stefan Monnier's avatar
Stefan Monnier committed
560
See `wdired-use-dired-vertical-movement'.  Optional prefix ARG
Stefan Monnier's avatar
Stefan Monnier committed
561 562
says how many lines to move; default is one line."
  (interactive "p")
563
  (with-no-warnings (next-line arg))
Stefan Monnier's avatar
Stefan Monnier committed
564 565
  (if (or (eq wdired-use-dired-vertical-movement t)
	  (and wdired-use-dired-vertical-movement
Stefan Monnier's avatar
Stefan Monnier committed
566 567 568 569 570 571 572
	       (< (current-column)
		  (save-excursion (dired-move-to-filename)
				  (current-column)))))
      (dired-move-to-filename)))

(defun wdired-previous-line (arg)
  "Move up lines then position at filename or the current column.
Stefan Monnier's avatar
Stefan Monnier committed
573
See `wdired-use-dired-vertical-movement'.  Optional prefix ARG
Stefan Monnier's avatar
Stefan Monnier committed
574 575
says how many lines to move; default is one line."
  (interactive "p")
576
  (with-no-warnings (previous-line arg))
Stefan Monnier's avatar
Stefan Monnier committed
577 578
  (if (or (eq wdired-use-dired-vertical-movement t)
	  (and wdired-use-dired-vertical-movement
Stefan Monnier's avatar
Stefan Monnier committed
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
	       (< (current-column)
		  (save-excursion (dired-move-to-filename)
				  (current-column)))))
      (dired-move-to-filename)))

;; Put the needed properties to allow the user to change links' targets
(defun wdired-preprocess-symlinks ()
  (let ((inhibit-read-only t))
    (save-excursion
      (goto-char (point-min))
      (while (not (eobp))
        (if (looking-at dired-re-sym)
            (progn
              (re-search-forward " -> \\(.*\\)$")
	      (put-text-property (- (match-beginning 1) 2)
				 (1- (match-beginning 1)) 'old-link
				 (match-string-no-properties 1))
              (put-text-property (match-end 1) (1+ (match-end 1)) 'end-link t)
597 598 599 600
              (put-text-property (1- (match-beginning 1))
				 (match-beginning 1)
				 'rear-nonsticky '(read-only))
	      (put-text-property (match-beginning 1)
Stefan Monnier's avatar
Stefan Monnier committed
601 602 603 604 605 606 607 608
				 (match-end 1) 'read-only nil)))
        (forward-line)
	(beginning-of-line)))))


(defun wdired-get-previous-link (&optional old move)
  "Return the next symlink target.
If OLD, return the old target.  If MOVE, move point before it."
609 610 611 612 613 614 615 616 617 618 619
  (let (beg end target)
    (setq beg (previous-single-property-change (point) 'old-link nil))
    (if beg
	(progn
	  (if old
	      (setq target (get-text-property (1- beg) 'old-link))
	    (setq end (next-single-property-change beg 'end-link))
	    (setq target (buffer-substring-no-properties (1+ beg) end)))
	  (if move (goto-char (1- beg)))))
    (and target (wdired-normalize-filename target))))

620
(declare-function make-symbolic-link "fileio.c")
Stefan Monnier's avatar
Stefan Monnier committed
621 622

;; Perform the changes in the target of the changed links.
Stefan Monnier's avatar
Stefan Monnier committed
623
(defun wdired-do-symlink-changes ()
Stefan Monnier's avatar
Stefan Monnier committed
624 625 626 627 628 629 630
  (let ((changes nil)
	(errors 0)
	link-to-ori link-to-new link-from)
    (goto-char (point-max))
    (while (setq link-to-new (wdired-get-previous-link))
      (setq link-to-ori (wdired-get-previous-link t t))
      (setq link-from (wdired-get-filename nil t))
Stefan Monnier's avatar
Stefan Monnier committed
631 632 633 634 635 636 637 638 639 640 641 642 643 644
      (unless (equal link-to-new link-to-ori)
        (setq changes t)
        (if (equal link-to-new "") ;empty filename!
            (setq link-to-new "/dev/null"))
        (condition-case err
            (progn
              (delete-file link-from)
              (make-symbolic-link
               (substitute-in-file-name link-to-new) link-from))
          (error
           (setq errors (1+ errors))
           (dired-log (concat "Link `" link-from "' to `"
                              link-to-new "' failed:\n%s\n")
                      err)))))
Stefan Monnier's avatar
Stefan Monnier committed
645 646 647 648 649 650
    (cons changes errors)))

;; Perform a "case command" skipping read-only words.
(defun wdired-xcase-word (command arg)
  (if (< arg 0)
      (funcall command arg)
Stefan Monnier's avatar
Stefan Monnier committed
651 652 653 654 655 656
    (while (> arg 0)
      (condition-case err
          (progn
            (funcall command 1)
            (setq arg (1- arg)))
        (error
657 658 659 660 661
         (if (forward-word)
	     ;; Skip any non-word characters to avoid triggering a read-only
	     ;; error which would cause skipping the next word characters too.
	     (skip-syntax-forward "^w")
	   (setq arg 0)))))))
Stefan Monnier's avatar
Stefan Monnier committed
662 663

(defun wdired-downcase-word (arg)
Stefan Monnier's avatar
Stefan Monnier committed
664
  "WDired version of `downcase-word'.
Stefan Monnier's avatar
Stefan Monnier committed
665 666 667 668 669
Like original function but it skips read-only words."
  (interactive "p")
  (wdired-xcase-word 'downcase-word arg))

(defun wdired-upcase-word (arg)
Stefan Monnier's avatar
Stefan Monnier committed
670
  "WDired version of `upcase-word'.
Stefan Monnier's avatar
Stefan Monnier committed
671 672 673 674 675
Like original function but it skips read-only words."
  (interactive "p")
  (wdired-xcase-word 'upcase-word arg))

(defun wdired-capitalize-word (arg)
Stefan Monnier's avatar
Stefan Monnier committed
676
  "WDired version of `capitalize-word'.
Stefan Monnier's avatar
Stefan Monnier committed
677 678 679 680 681 682 683 684
Like original function but it skips read-only words."
  (interactive "p")
  (wdired-xcase-word 'capitalize-word arg))


;; The following code deals with changing the access bits (or
;; permissions) of the files.

Stefan Monnier's avatar
Stefan Monnier committed
685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
(defvar wdired-perm-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map " " 'wdired-toggle-bit)
    (define-key map "r" 'wdired-set-bit)
    (define-key map "w" 'wdired-set-bit)
    (define-key map "x" 'wdired-set-bit)
    (define-key map "-" 'wdired-set-bit)
    (define-key map "S" 'wdired-set-bit)
    (define-key map "s" 'wdired-set-bit)
    (define-key map "T" 'wdired-set-bit)
    (define-key map "t" 'wdired-set-bit)
    (define-key map "s" 'wdired-set-bit)
    (define-key map "l" 'wdired-set-bit)
    (define-key map [down-mouse-1] 'wdired-mouse-toggle-bit)
    map))
Stefan Monnier's avatar
Stefan Monnier committed
700

701
;; Put a keymap property to the permission bits of the files, and store the
Stefan Monnier's avatar
Stefan Monnier committed
702
;; original name and permissions as a property
Stefan Monnier's avatar
Stefan Monnier committed
703
(defun wdired-preprocess-perms ()
704
  (let ((inhibit-read-only t))
Stefan Monnier's avatar
Stefan Monnier committed
705 706 707 708
    (set (make-local-variable 'wdired-col-perm) nil)
    (save-excursion
      (goto-char (point-min))
      (while (not (eobp))
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728
	(when (and (not (looking-at dired-re-sym))
		   (wdired-get-filename)
		   (re-search-forward dired-re-perms (line-end-position) 'eol))
	  (let ((begin (match-beginning 0))
		(end (match-end 0)))
	    (unless wdired-col-perm
	      (setq wdired-col-perm (- (current-column) 9)))
	    (if (eq wdired-allow-to-change-permissions 'advanced)
		(progn
		  (put-text-property begin end 'read-only nil)
		  ;; make first permission bit writable
		  (put-text-property
		   (1- begin) begin 'rear-nonsticky '(read-only)))
	      ;; avoid that keymap applies to text following permissions
	      (add-text-properties
	       (1+ begin) end
	       `(keymap ,wdired-perm-mode-map rear-nonsticky (keymap))))
	    (put-text-property end (1+ end) 'end-perm t)
	    (put-text-property
	     begin (1+ begin) 'old-perm (match-string-no-properties 0))))
Stefan Monnier's avatar
Stefan Monnier committed
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
        (forward-line)
	(beginning-of-line)))))

(defun wdired-perm-allowed-in-pos (char pos)
  (cond
   ((= char ?-)          t)
   ((= char ?r)          (= (% pos 3) 0))
   ((= char ?w)          (= (% pos 3) 1))
   ((= char ?x)          (= (% pos 3) 2))
   ((memq char '(?s ?S)) (memq pos '(2 5)))
   ((memq char '(?t ?T)) (= pos 8))
   ((= char ?l)          (= pos 5))))

(defun wdired-set-bit ()
  "Set a permission bit character."
  (interactive)
745
  (if (wdired-perm-allowed-in-pos last-command-event
Stefan Monnier's avatar
Stefan Monnier committed
746
                                  (- (current-column) wdired-col-perm))
747
      (let ((new-bit (char-to-string last-command-event))
Stefan Monnier's avatar
Stefan Monnier committed
748 749
            (inhibit-read-only t)
	    (pos-prop (- (point) (- (current-column) wdired-col-perm))))
Stefan Monnier's avatar
Stefan Monnier committed
750
        (put-text-property 0 1 'keymap wdired-perm-mode-map new-bit)
Stefan Monnier's avatar
Stefan Monnier committed
751 752 753
        (put-text-property 0 1 'read-only t new-bit)
        (insert new-bit)
        (delete-char 1)
754 755
	(put-text-property (1- pos-prop) pos-prop 'perm-changed t)
	(put-text-property (1- (point)) (point) 'rear-nonsticky '(keymap)))
Stefan Monnier's avatar
Stefan Monnier committed
756 757
    (forward-char 1)))

Stefan Monnier's avatar
Stefan Monnier committed
758
(defun wdired-toggle-bit ()
Stefan Monnier's avatar
Stefan Monnier committed
759 760 761
  "Toggle the permission bit at point."
  (interactive)
  (let ((inhibit-read-only t)
762
	(new-bit "-")
Stefan Monnier's avatar
Stefan Monnier committed
763
	(pos-prop (- (point) (- (current-column) wdired-col-perm))))
764
    (if (eq (char-after (point)) ?-)
765
	(setq new-bit
766 767 768
	      (if (= (% (- (current-column) wdired-col-perm) 3) 0) "r"
		(if (= (% (- (current-column) wdired-col-perm) 3) 1) "w"
		  "x"))))
Stefan Monnier's avatar
Stefan Monnier committed
769
    (put-text-property 0 1 'keymap wdired-perm-mode-map new-bit)
Stefan Monnier's avatar
Stefan Monnier committed
770 771 772
    (put-text-property 0 1 'read-only t new-bit)
    (insert new-bit)
    (delete-char 1)
773 774
    (put-text-property (1- pos-prop) pos-prop 'perm-changed t)
    (put-text-property (1- (point)) (point) 'rear-nonsticky '(keymap))))
Stefan Monnier's avatar
Stefan Monnier committed
775 776 777 778 779 780 781 782 783 784 785

(defun wdired-mouse-toggle-bit (event)
  "Toggle the permission bit that was left clicked."
  (interactive "e")
  (mouse-set-point event)
  (wdired-toggle-bit))

;; Allowed chars for 4000 bit are Ss  in position 3
;; Allowed chars for 2000 bit are Ssl in position 6
;; Allowed chars for 1000 bit are Tt  in position 9
(defun wdired-perms-to-number (perms)
786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
  (let ((nperm 0777))
    (if (= (elt perms 1) ?-) (setq nperm (- nperm 400)))
    (if (= (elt perms 2) ?-) (setq nperm (- nperm 200)))
    (let ((p-bit (elt perms 3)))
      (if (memq p-bit '(?- ?S)) (setq nperm (- nperm 100)))
      (if (memq p-bit '(?s ?S)) (setq nperm (+ nperm 4000))))
    (if (= (elt perms 4) ?-) (setq nperm (- nperm 40)))
    (if (= (elt perms 5) ?-) (setq nperm (- nperm 20)))
    (let ((p-bit (elt perms 6)))
      (if (memq p-bit '(?- ?S ?l)) (setq nperm (- nperm 10)))
      (if (memq p-bit '(?s ?S ?l)) (setq nperm (+ nperm 2000))))
    (if (= (elt perms 7) ?-) (setq nperm (- nperm 4)))
    (if (= (elt perms 8) ?-) (setq nperm (- nperm 2)))
    (let ((p-bit (elt perms 9)))
      (if (memq p-bit '(?- ?T)) (setq nperm (- nperm 1)))
      (if (memq p-bit '(?t ?T)) (setq nperm (+ nperm 1000))))
    nperm))
Stefan Monnier's avatar
Stefan Monnier committed
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817

;; Perform the changes in the permissions of the files that have
;; changed.
(defun wdired-do-perm-changes ()
  (let ((changes nil)
	(errors 0)
	(prop-wanted (if (eq wdired-allow-to-change-permissions 'advanced)
			 'old-perm 'perm-changed))
	filename perms-ori perms-new perm-tmp)
    (goto-char (next-single-property-change (point-min) prop-wanted
					    nil (point-max)))
    (while (not (eobp))
      (setq perms-ori (get-text-property (point) 'old-perm))
      (setq perms-new (buffer-substring-no-properties
		       (point) (next-single-property-change (point) 'end-perm)))
Stefan Monnier's avatar
Stefan Monnier committed
818 819 820 821 822 823 824
      (unless (equal perms-ori perms-new)
        (setq changes t)
        (setq filename (wdired-get-filename nil t))
        (if (= (length perms-new) 10)
            (progn
              (setq perm-tmp
                    (int-to-string (wdired-perms-to-number perms-new)))
825 826
              (unless (equal 0 (process-file dired-chmod-program
					     nil nil nil perm-tmp filename))
Stefan Monnier's avatar
Stefan Monnier committed
827 828 829 830 831 832
                (setq errors (1+ errors))
                (dired-log (concat dired-chmod-program " " perm-tmp
                                   " `" filename "' failed\n\n"))))
          (setq errors (1+ errors))
          (dired-log (concat "Cannot parse permission `" perms-new
                             "' for file `" filename "'\n\n"))))
Stefan Monnier's avatar
Stefan Monnier committed
833 834 835 836 837 838
      (goto-char (next-single-property-change (1+ (point)) prop-wanted
					      nil (point-max))))
    (cons changes errors)))

(provide 'wdired)

Stefan Monnier's avatar
Stefan Monnier committed
839 840 841 842 843
;; Local Variables:
;; coding: latin-1
;; byte-compile-dynamic: t
;; End:

Stefan Monnier's avatar
Stefan Monnier committed
844
;;; wdired.el ends here