wdired.el 35.7 KB
Newer Older
1
;;; wdired.el --- Rename files editing their names in dired buffers -*- coding: utf-8; -*-
Stefan Monnier's avatar
Stefan Monnier committed
2

Paul Eggert's avatar
Paul Eggert committed
3
;; Copyright (C) 2004-2019 Free Software Foundation, Inc.
Stefan Monnier's avatar
Stefan Monnier committed
4 5

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

;; This file is part of GNU Emacs.

12 13 14 15
;; 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
16

17 18 19 20
;; 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
21 22

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

;;; 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
31
;; (query-replace), M-c (capitalize-word), etc... to change the name of
Stefan Monnier's avatar
Stefan Monnier committed
32 33
;; 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
34
;;
Juanma Barranquero's avatar
Juanma Barranquero committed
35
;; This package provides a function that makes the filenames of a
Stefan Monnier's avatar
Stefan Monnier committed
36 37 38 39 40
;; 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
41
;; Another things you can do with WDired:
Stefan Monnier's avatar
Stefan Monnier committed
42 43 44 45 46 47 48 49
;;
;; - 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
50
;;   variable `wdired-allow-to-change-permissions'.  To change a single
Stefan Monnier's avatar
Stefan Monnier committed
51
;;   char (toggling between its two more usual values) you can press
Stefan Monnier's avatar
Stefan Monnier committed
52
;;   the space bar over it or left-click the mouse.  To set any char to
Stefan Monnier's avatar
Stefan Monnier committed
53
;;   an specific value (this includes the SUID, SGID and STI bits) you
Stefan Monnier's avatar
Stefan Monnier committed
54
;;   can use the key labeled as the letter you want.  Please note that
Stefan Monnier's avatar
Stefan Monnier committed
55 56 57 58 59 60 61 62
;;   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.

;;; Usage:

63 64 65 66 67
;; You can edit the names of the files by typing C-x C-q or by
;; executing M-x wdired-change-to-wdired-mode.  Use C-c C-c when
;; finished or C-c C-k to abort.  While editing filenames, a new
;; submenu "WDired" is available at top level.  You can customize the
;; behavior of this package from this menu.
Stefan Monnier's avatar
Stefan Monnier committed
68 69 70

;;; Change Log:

Stefan Monnier's avatar
Stefan Monnier committed
71 72
;; Google is your friend (previous versions with complete changelogs
;; were posted to gnu.emacs.sources)
Stefan Monnier's avatar
Stefan Monnier committed
73 74 75

;;; Code:

Stefan Monnier's avatar
Stefan Monnier committed
76 77
(require 'dired)
(autoload 'dired-do-create-files-regexp "dired-aux")
Stefan Monnier's avatar
Stefan Monnier committed
78 79 80 81 82 83

(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
84
  "If non-nil, WDired requires confirmation before actually renaming files.
Stefan Monnier's avatar
Stefan Monnier committed
85 86 87
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
88 89 90
  :type 'boolean
  :group 'wdired)

Stefan Monnier's avatar
Stefan Monnier committed
91
(defcustom wdired-confirm-overwrite t
Stefan Monnier's avatar
Stefan Monnier committed
92
  "If nil the renames can overwrite files without asking.
Stefan Monnier's avatar
Stefan Monnier committed
93 94
This variable has no effect at all if `wdired-use-interactive-rename'
is not nil."
Stefan Monnier's avatar
Stefan Monnier committed
95 96 97
  :type 'boolean
  :group 'wdired)

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

102 103
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
104 105 106 107 108 109 110 111 112
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
113
  "If non-nil, the target of the symbolic links are editable.
Stefan Monnier's avatar
Stefan Monnier committed
114 115 116 117 118 119
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
120
  "If non-nil, the permissions bits of the files are editable.
Stefan Monnier's avatar
Stefan Monnier committed
121 122 123 124 125

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
126
you can keep the <x> key pressed to give execution permissions to
Stefan Monnier's avatar
Stefan Monnier committed
127 128
everybody to that file.

129
If `advanced', the bits are freely editable.  You can use
Stefan Monnier's avatar
Stefan Monnier committed
130 131 132 133
`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
134
Anyway, the real change of the permissions is done by the external
Stefan Monnier's avatar
Stefan Monnier committed
135 136
program `dired-chmod-program', which must exist."
  :type '(choice (const :tag "Not allowed" nil)
137
                 (const :tag "Toggle/set bits" t)
Stefan Monnier's avatar
Stefan Monnier committed
138 139 140
		 (other :tag "Bits freely editable" advanced))
  :group 'wdired)

141 142 143 144 145 146 147 148 149 150 151 152 153 154
(defcustom wdired-keep-marker-rename t
  ;; Use t as default so that renamed files "take their markers with them".
  "Controls marking of files renamed in WDired.
If t, files keep their previous marks when they are renamed.
If a character, renamed files (whether previously marked or not)
are afterward marked with that character.
This option affects only files renamed by `wdired-finish-edit'.
See `dired-keep-marker-rename' if you want to do the same for files
renamed by `dired-do-rename' and `dired-do-rename-regexp'."
  :type '(choice (const :tag "Keep" t)
		 (character :tag "Mark" :value ?R))
  :version "24.3"
  :group 'wdired)

155 156 157 158 159 160
(defcustom wdired-create-parent-directories t
  "If non-nil, create parent directories of destination files.
If non-nil, when you rename a file to a destination path within a
nonexistent directory, wdired will create any parent directories
necessary.  When nil, attempts to rename a file into a
nonexistent directory will fail."
161
  :version "26.1"
162 163 164
  :type 'boolean
  :group 'wdired)

165 166 167 168 169 170
(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
171
    (define-key map "\C-x\C-q" 'wdired-exit)
Stefan Monnier's avatar
Stefan Monnier committed
172 173 174
    (define-key map "\C-m"     'ignore)
    (define-key map "\C-j"     'ignore)
    (define-key map "\C-o"     'ignore)
175 176 177 178 179 180 181 182 183 184
    (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
185
      '("--"))
186
    (define-key map [menu-bar wdired wdired-abort-changes]
187 188
      '(menu-item "Abort Changes" wdired-abort-changes
		  :help "Abort changes and return to dired mode"))
189 190
    (define-key map [menu-bar wdired wdired-finish-edit]
      '("Commit Changes" . wdired-finish-edit))
Stefan Monnier's avatar
Stefan Monnier committed
191 192 193 194 195

    (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)

Glenn Morris's avatar
Glenn Morris committed
196 197
    map)
  "Keymap used in `wdired-mode'.")
198 199

(defvar wdired-mode-hook nil
Stefan Monnier's avatar
Stefan Monnier committed
200
  "Hooks run when changing to WDired mode.")
Stefan Monnier's avatar
Stefan Monnier committed
201 202 203 204

;; Local variables (put here to avoid compilation gripes)
(defvar wdired-col-perm) ;; Column where the permission bits start
(defvar wdired-old-content)
205
(defvar wdired-old-point)
206
(defvar wdired-old-marks)
Stefan Monnier's avatar
Stefan Monnier committed
207 208

(defun wdired-mode ()
209
  "Writable Dired (WDired) mode.
Chong Yidong's avatar
Chong Yidong committed
210
\\<wdired-mode-map>
211 212 213
In WDired mode, you can edit the names of the files in the
buffer, the target of the links, and the permission bits of the
files.
Stefan Monnier's avatar
Stefan Monnier committed
214

215 216 217 218 219 220 221 222 223
Type \\[wdired-finish-edit] to exit WDired mode, returning to
Dired mode, and make your edits \"take effect\" by modifying the
file and directory names, link targets, and/or file permissions
on disk.  If you delete the filename of a file, it is flagged for
deletion in the Dired buffer.

Type \\[wdired-abort-changes] to abort your edits and exit WDired mode.

Type \\[customize-group] RET wdired to customize WDired behavior.
Stefan Monnier's avatar
Stefan Monnier committed
224

Stefan Monnier's avatar
Stefan Monnier committed
225 226
The only editable texts in a WDired buffer are filenames,
symbolic link targets, and filenames permission."
Stefan Monnier's avatar
Stefan Monnier committed
227 228 229 230 231 232 233
  (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 ()
234
  "Put a Dired buffer in Writable Dired (WDired) mode.
Stefan Monnier's avatar
Stefan Monnier committed
235
\\<wdired-mode-map>
236 237 238 239
In WDired mode, you can edit the names of the files in the
buffer, the target of the links, and the permission bits of the
files.  After typing \\[wdired-finish-edit], Emacs modifies the files and
directories to reflect your edits.
Stefan Monnier's avatar
Stefan Monnier committed
240 241 242

See `wdired-mode'."
  (interactive)
243 244
  (unless (eq major-mode 'dired-mode)
    (error "Not a Dired buffer"))
Stefan Monnier's avatar
Stefan Monnier committed
245 246
  (set (make-local-variable 'wdired-old-content)
       (buffer-substring (point-min) (point-max)))
247 248
  (set (make-local-variable 'wdired-old-marks)
       (dired-remember-marks (point-min) (point-max)))
249
  (set (make-local-variable 'wdired-old-point) (point))
250
  (set (make-local-variable 'query-replace-skip-read-only) t)
251 252
  (add-function :after-while (local 'isearch-filter-predicate)
                #'wdired-isearch-filter-read-only)
Stefan Monnier's avatar
Stefan Monnier committed
253
  (use-local-map wdired-mode-map)
254
  (force-mode-line-update)
Stefan Monnier's avatar
Stefan Monnier committed
255 256 257
  (setq buffer-read-only nil)
  (dired-unadvertise default-directory)
  (add-hook 'kill-buffer-hook 'wdired-check-kill-buffer nil t)
258
  (add-hook 'after-change-functions 'wdired--restore-dired-filename-prop nil t)
Stefan Monnier's avatar
Stefan Monnier committed
259
  (setq major-mode 'wdired-mode)
Stefan Monnier's avatar
Stefan Monnier committed
260
  (setq mode-name "Editable Dired")
Stefan Monnier's avatar
Stefan Monnier committed
261 262 263 264 265 266 267 268 269 270 271 272 273
  (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)
274
  (run-mode-hooks 'wdired-mode-hook)
275 276
  (message "%s" (substitute-command-keys
		 "Press \\[wdired-finish-edit] when finished \
Juri Linkov's avatar
Juri Linkov committed
277
or \\[wdired-abort-changes] to abort changes")))
Stefan Monnier's avatar
Stefan Monnier committed
278

279 280
(defun wdired-isearch-filter-read-only (beg end)
  "Skip matches that have a read-only property."
281 282
  (not (text-property-not-all (min beg end) (max beg end)
			      'read-only nil)))
Stefan Monnier's avatar
Stefan Monnier committed
283 284 285 286

;; 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 ()
287
  (put-text-property (point-min) (1+ (point-min))'front-sticky t)
Stefan Monnier's avatar
Stefan Monnier committed
288 289 290 291 292 293
  (save-excursion
    (goto-char (point-min))
    (let ((b-protection (point))
	  filename)
      (while (not (eobp))
	(setq filename (dired-get-filename nil t))
294 295 296
        (when (and filename
		   (not (member (file-name-nondirectory filename) '("." ".."))))
	  (dired-move-to-filename)
297 298 299 300 301 302 303
	  ;; 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
304 305 306 307
        (forward-line))
      (put-text-property b-protection (point-max) 'read-only t))))

;; This code is a copy of some dired-get-filename lines.
308 309 310 311 312 313 314 315 316
(defsubst wdired-normalize-filename (file unquotep)
  (when unquotep
    (setq file
          ;; FIXME: shouldn't we check for a `b' argument or somesuch before
          ;; doing such unquoting?  --Stef
          (read (concat
                 "\"" (replace-regexp-in-string
                       "\\([^\\]\\|\\`\\)\"" "\\1\\\\\"" file)
                 "\""))))
Stefan Monnier's avatar
Stefan Monnier committed
317 318 319 320 321 322 323 324
  (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.
325
Similar to `dired-get-filename' but it doesn't rely on regexps.  It
Stefan Monnier's avatar
Stefan Monnier committed
326
relies on WDired buffer's properties.  Optional arg NO-DIR with value
Stefan Monnier's avatar
Stefan Monnier committed
327 328
non-nil means don't include directory.  Optional arg OLD with value
non-nil means return old filename."
329
  ;; FIXME: Use dired-get-filename's new properties.
330 331 332 333 334 335 336 337
  (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))
338 339 340 341 342
	  ;; 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))
343
	  (setq file (buffer-substring-no-properties (1+ beg) end)))
344 345
	;; Don't unquote the old name, it wasn't quoted in the first place
        (and file (setq file (wdired-normalize-filename file (not old)))))
346 347 348 349
      (if (or no-dir old)
	  file
	(and file (> (length file) 0)
             (concat (dired-current-directory) file))))))
Stefan Monnier's avatar
Stefan Monnier committed
350 351 352 353


(defun wdired-change-to-dired-mode ()
  "Change the mode back to dired."
354 355
  (or (eq major-mode 'wdired-mode)
      (error "Not a Wdired buffer"))
Stefan Monnier's avatar
Stefan Monnier committed
356
  (let ((inhibit-read-only t))
357 358 359
    (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
360
  (use-local-map dired-mode-map)
361
  (force-mode-line-update)
Stefan Monnier's avatar
Stefan Monnier committed
362 363 364 365 366
  (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)
367
  (remove-hook 'after-change-functions 'wdired--restore-dired-filename-prop t)
Stefan Monnier's avatar
Stefan Monnier committed
368
  (set (make-local-variable 'revert-buffer-function) 'dired-revert))
Stefan Monnier's avatar
Stefan Monnier committed
369 370 371 372 373 374 375


(defun wdired-abort-changes ()
  "Abort changes and return to dired mode."
  (interactive)
  (let ((inhibit-read-only t))
    (erase-buffer)
376 377
    (insert wdired-old-content)
    (goto-char wdired-old-point))
Stefan Monnier's avatar
Stefan Monnier committed
378 379
  (wdired-change-to-dired-mode)
  (set-buffer-modified-p nil)
380 381
  (setq buffer-undo-list nil)
  (message "Changes aborted"))
Stefan Monnier's avatar
Stefan Monnier committed
382 383 384 385

(defun wdired-finish-edit ()
  "Actually rename files based on your editing in the Dired buffer."
  (interactive)
386
  (let ((changes nil)
Stefan Monnier's avatar
Stefan Monnier committed
387
	(errors 0)
388 389 390 391
	files-deleted
	files-renamed
	some-file-names-unchanged
	file-old file-new tmp-value)
Stefan Monnier's avatar
Stefan Monnier committed
392
    (save-excursion
393 394 395 396 397 398 399 400 401 402
      (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
403 404
      (goto-char (point-max))
      (while (not (bobp))
405 406
	(setq file-old (wdired-get-filename nil t))
	(when file-old
407
	  (setq file-new (wdired-get-filename))
408 409
          (if (equal file-new file-old)
	      (setq some-file-names-unchanged t)
410 411
            (setq changes t)
            (if (not file-new)		;empty filename!
412
                (push file-old files-deleted)
413 414 415 416 417 418 419 420 421
	      (when wdired-keep-marker-rename
		(let ((mark (cond ((integerp wdired-keep-marker-rename)
				   wdired-keep-marker-rename)
				  (wdired-keep-marker-rename
				   (cdr (assoc file-old wdired-old-marks)))
				  (t nil))))
		  (when mark
		    (push (cons (substitute-in-file-name file-new) mark)
			  wdired-old-marks))))
422 423
              (push (cons file-old (substitute-in-file-name file-new))
                    files-renamed))))
Stefan Monnier's avatar
Stefan Monnier committed
424
	(forward-line -1)))
425 426
    (when files-renamed
      (setq errors (+ errors (wdired-do-renames files-renamed))))
427 428 429 430 431
    ;; We have to be in wdired-mode when wdired-do-renames is executed
    ;; so that wdired--restore-dired-filename-prop runs, but we have
    ;; to change back to dired-mode before reverting the buffer to
    ;; avoid using wdired-revert, which changes back to wdired-mode.
    (wdired-change-to-dired-mode)
Stefan Monnier's avatar
Stefan Monnier committed
432
    (if changes
433 434 435 436 437 438 439 440 441 442 443
	(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.
444 445 446
	  (revert-buffer)
	  (let ((inhibit-read-only t))
	    (dired-mark-remembered wdired-old-marks)))
Stefan Monnier's avatar
Stefan Monnier committed
447
      (let ((inhibit-read-only t))
Stefan Monnier's avatar
Stefan Monnier committed
448 449 450 451 452
	(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)")))
453 454 455 456
    (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
457 458 459
  (set-buffer-modified-p nil)
  (setq buffer-undo-list nil))

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 489 490 491 492 493 494 495 496 497 498 499 500
(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)
501
          (let ((file-ori (car rename)))
502 503 504 505 506 507 508 509 510
            (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))
511 512
                    (and wdired-create-parent-directories
                         (wdired-create-parentdirs file-new))
513
                    (dired-rename-file file-ori file-new
514
                                       overwrite))
515 516
                (error
                 (setq errors (1+ errors))
517
                 (dired-log "Rename `%s' to `%s' failed:\n%s\n"
518
                            file-ori file-new
519
                            err)))))))))
520
    errors))
Juanma Barranquero's avatar
Juanma Barranquero committed
521

522 523 524 525 526
(defun wdired-create-parentdirs (file-new)
  "Create parent directories for FILE-NEW if they don't exist."
  (and (not (file-exists-p (file-name-directory file-new)))
       (message "Creating directory for file %s" file-new)
       (make-directory (file-name-directory file-new) t)))
527

Juri Linkov's avatar
Juri Linkov committed
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
(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
544
;; Rename a file, searching it in a modified dired buffer, in order
Stefan Monnier's avatar
Stefan Monnier committed
545
;; to be able to use `dired-do-create-files-regexp' and get its
Stefan Monnier's avatar
Stefan Monnier committed
546
;; "benefits".
Stefan Monnier's avatar
Stefan Monnier committed
547 548 549 550
(defun wdired-search-and-rename (filename-ori filename-new)
  (save-excursion
    (goto-char (point-max))
    (forward-line -1)
551
    (let ((done nil)
552
          (failed t)
Stefan Monnier's avatar
Stefan Monnier committed
553
	  curr-filename)
554 555 556
      (while (and (not done) (not (bobp)))
        (setq curr-filename (wdired-get-filename nil t))
        (if (equal curr-filename filename-ori)
557 558 559 560 561 562 563 564 565 566 567 568 569 570
            (unwind-protect
                (progn
                  (setq done t)
                  (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)
                  (setq failed nil))
              ;; If user types C-g when prompted to change the file
              ;; name, make sure we return to dired-mode.
              (when failed (wdired-change-to-dired-mode)))
571
	  (forward-line -1))))))
Stefan Monnier's avatar
Stefan Monnier committed
572 573 574 575 576 577 578 579 580 581 582

;; 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
583
  "Customize WDired options."
Stefan Monnier's avatar
Stefan Monnier committed
584 585 586
  (interactive)
  (customize-apropos "wdired" 'groups))

587
(defun wdired-revert (&optional _arg _noconfirm)
Stefan Monnier's avatar
Stefan Monnier committed
588 589
  "Discard changes in the buffer and update it based on changes on disk.
Optional arguments are ignored."
Stefan Monnier's avatar
Stefan Monnier committed
590 591 592 593 594
  (wdired-change-to-dired-mode)
  (revert-buffer)
  (wdired-change-to-wdired-mode))

(defun wdired-check-kill-buffer ()
595
  ;; FIXME: Can't we use the normal mechanism for that?  --Stef
Stefan Monnier's avatar
Stefan Monnier committed
596 597 598
  (if (and
       (buffer-modified-p)
       (not (y-or-n-p "Buffer changed. Discard changes and kill buffer? ")))
Juanma Barranquero's avatar
Juanma Barranquero committed
599
      (error "Error")))
Stefan Monnier's avatar
Stefan Monnier committed
600

601 602 603 604 605 606 607 608 609
;; Added to after-change-functions in wdired-change-to-wdired-mode to
;; ensure that, on editing a file name, new characters get the
;; dired-filename text property, which allows functions that look for
;; this property (e.g. dired-isearch-filenames) to work in wdired-mode
;; and also avoids an error with non-nil wdired-use-interactive-rename
;; (bug#32173).
(defun wdired--restore-dired-filename-prop (beg end _len)
  (save-match-data
    (save-excursion
610 611 612 613 614
      (let ((lep (line-end-position)))
        (beginning-of-line)
        (when (re-search-forward
               directory-listing-before-filename-regexp lep t)
          (setq beg (point)
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
                end (if (or
                         ;; If the file is a symlink, put the
                         ;; dired-filename property only on the link
                         ;; name.  (Using (file-symlink-p
                         ;; (dired-get-filename)) fails in
                         ;; wdired-mode, bug#32673.)
                         (and (re-search-backward
                               dired-permission-flags-regexp nil t)
                              (looking-at "l")
                              (search-forward " -> " lep t))
                         ;; When dired-listing-switches includes "F"
                         ;; or "classify", don't treat appended
                         ;; indicator characters as part of the file
                         ;; name (bug#34915).
                         (and (dired-check-switches dired-actual-switches
                                                    "F" "classify")
                              (re-search-forward "[*/@|=>]$" lep t)))
632 633 634
                        (goto-char (match-beginning 0))
                      lep))
          (put-text-property beg end 'dired-filename t))))))
635

Stefan Monnier's avatar
Stefan Monnier committed
636 637
(defun wdired-next-line (arg)
  "Move down lines then position at filename or the current column.
Stefan Monnier's avatar
Stefan Monnier committed
638
See `wdired-use-dired-vertical-movement'.  Optional prefix ARG
Stefan Monnier's avatar
Stefan Monnier committed
639
says how many lines to move; default is one line."
640
  (interactive "^p")
641
  (with-no-warnings (next-line arg))
Stefan Monnier's avatar
Stefan Monnier committed
642 643
  (if (or (eq wdired-use-dired-vertical-movement t)
	  (and wdired-use-dired-vertical-movement
Stefan Monnier's avatar
Stefan Monnier committed
644 645 646 647 648 649 650
	       (< (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
651
See `wdired-use-dired-vertical-movement'.  Optional prefix ARG
Stefan Monnier's avatar
Stefan Monnier committed
652
says how many lines to move; default is one line."
653
  (interactive "^p")
654
  (with-no-warnings (previous-line arg))
Stefan Monnier's avatar
Stefan Monnier committed
655 656
  (if (or (eq wdired-use-dired-vertical-movement t)
	  (and wdired-use-dired-vertical-movement
Stefan Monnier's avatar
Stefan Monnier committed
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
	       (< (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)
675 676 677 678
              (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
679 680 681 682 683 684 685 686
				 (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."
687 688 689 690 691 692 693 694 695
  (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)))))
696
    (and target (wdired-normalize-filename target t))))
697

698
(declare-function make-symbolic-link "fileio.c")
Stefan Monnier's avatar
Stefan Monnier committed
699 700

;; Perform the changes in the target of the changed links.
Stefan Monnier's avatar
Stefan Monnier committed
701
(defun wdired-do-symlink-changes ()
Stefan Monnier's avatar
Stefan Monnier committed
702 703 704 705 706 707 708
  (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
709 710 711 712 713 714 715 716 717 718 719
      (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))
720
           (dired-log "Link `%s' to `%s' failed:\n%s\n"
721
                      link-from link-to-new
Stefan Monnier's avatar
Stefan Monnier committed
722
                      err)))))
Stefan Monnier's avatar
Stefan Monnier committed
723 724 725 726 727 728
    (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
729
    (while (> arg 0)
730
      (condition-case nil
Stefan Monnier's avatar
Stefan Monnier committed
731 732 733 734
          (progn
            (funcall command 1)
            (setq arg (1- arg)))
        (error
735
         (if (forward-word-strictly)
736 737 738 739
	     ;; 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
740 741

(defun wdired-downcase-word (arg)
Stefan Monnier's avatar
Stefan Monnier committed
742
  "WDired version of `downcase-word'.
Stefan Monnier's avatar
Stefan Monnier committed
743 744 745 746 747
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
748
  "WDired version of `upcase-word'.
Stefan Monnier's avatar
Stefan Monnier committed
749 750 751 752 753
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
754
  "WDired version of `capitalize-word'.
Stefan Monnier's avatar
Stefan Monnier committed
755 756 757 758 759 760 761 762
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
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777
(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
778

779
;; Put a keymap property to the permission bits of the files, and store the
Stefan Monnier's avatar
Stefan Monnier committed
780
;; original name and permissions as a property
Stefan Monnier's avatar
Stefan Monnier committed
781
(defun wdired-preprocess-perms ()
782
  (let ((inhibit-read-only t))
Stefan Monnier's avatar
Stefan Monnier committed
783 784 785 786
    (set (make-local-variable 'wdired-col-perm) nil)
    (save-excursion
      (goto-char (point-min))
      (while (not (eobp))
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
	(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
807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
        (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)
823
  (if (wdired-perm-allowed-in-pos last-command-event
Stefan Monnier's avatar
Stefan Monnier committed
824
                                  (- (current-column) wdired-col-perm))
825
      (let ((new-bit (char-to-string last-command-event))
Stefan Monnier's avatar
Stefan Monnier committed
826 827
            (inhibit-read-only t)
	    (pos-prop (- (point) (- (current-column) wdired-col-perm))))
Stefan Monnier's avatar
Stefan Monnier committed
828
        (put-text-property 0 1 'keymap wdired-perm-mode-map new-bit)
Stefan Monnier's avatar
Stefan Monnier committed
829 830 831
        (put-text-property 0 1 'read-only t new-bit)
        (insert new-bit)
        (delete-char 1)
832 833
	(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
834 835
    (forward-char 1)))

Stefan Monnier's avatar
Stefan Monnier committed
836
(defun wdired-toggle-bit ()
Stefan Monnier's avatar
Stefan Monnier committed
837 838 839
  "Toggle the permission bit at point."
  (interactive)
  (let ((inhibit-read-only t)
840
	(new-bit "-")
Stefan Monnier's avatar
Stefan Monnier committed
841
	(pos-prop (- (point) (- (current-column) wdired-col-perm))))
842
    (if (eq (char-after (point)) ?-)
843
	(setq new-bit
844 845 846
	      (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
847
    (put-text-property 0 1 'keymap wdired-perm-mode-map new-bit)
Stefan Monnier's avatar
Stefan Monnier committed
848 849 850
    (put-text-property 0 1 'read-only t new-bit)
    (insert new-bit)
    (delete-char 1)
851 852
    (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
853 854 855 856 857 858 859 860 861 862 863

(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)
864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
  (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
881 882 883 884 885 886 887 888 889 890 891 892 893 894 895

;; 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
896 897 898 899 900 901 902
      (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)))
903 904
              (unless (equal 0 (process-file dired-chmod-program
					     nil nil nil perm-tmp filename))
Stefan Monnier's avatar
Stefan Monnier committed
905
                (setq errors (1+ errors))
906
                (dired-log "%s %s `%s' failed\n\n"
907
                           dired-chmod-program perm-tmp filename)))
Stefan Monnier's avatar
Stefan Monnier committed
908
          (setq errors (1+ errors))
909
          (dired-log "Cannot parse permission `%s' for file `%s'\n\n"
910
                     perms-new filename)))
Stefan Monnier's avatar
Stefan Monnier committed
911 912 913 914 915 916
      (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
917 918 919 920
;; Local Variables:
;; byte-compile-dynamic: t
;; End:

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