ファイラからの削除に Windows の標準機能を使う

xyzzy のファイラは便利なんだけど、
大量のファイルが入ったフォルダを "D" でごみ箱に入れようとすると
ものすごく時間がかかる上、
各ファイルはごみ箱の中にばらばらに入れられて元に戻すのも大変。


ファイラでコンテキストメニューから『削除』を選んでやれば
いいだけなんだけど、それを xyzzy lisp から呼び出せないか調べてみた。
おそらく winapi を使えばいいのだろう。


で、調べてみたところ、どうやら shell32.dll 中にある
SHFileOperation という関数を使えばいいらしい(参考)。
引数が構造体なのでいろいろややこしかったが、
試行錯誤の結果以下のようなコードになった。

;;; ごみ箱へ送るを Windows のシェル API で。
(require "wip/winapi")

(in-package "win-user")

(*define-c-type (char *) LPCTSTR)
(*define-c-type u_short FILEOP_FLAGS)
(*define-c-struct LPSHFILEOPSTRUCT
  (HWND hwnd)
  (u_int wFunc)
  (LPCTSTR pFrom)
  (LPCTSTR pTo)
  (FILEOP_FLAGS fFlags)
  (BOOL fAnyOperationsAborted)
  (LPVOID hNameMappings)
  (LPCTSTR lpszProgressTitle))

(*define FO_DELETE #x0003)
(*define FOF_ALLOWUNDO #x0040)
(*define FOF_NOCONFIRMATION #x0010)

(define-dll-entry int SHFileOperation ((LPSHFILEOPSTRUCT *)) "shell32" "SHFileOperationA")

(defun filer-delete-api-1 (files)
  (let ((fileop (make-LPSHFILEOPSTRUCT))
        (filename (si:make-string-chunk (format nil "~{~A\0~}\0" files))))
    (setf (LPSHFILEOPSTRUCT-hwnd fileop) 0)
    (setf (LPSHFILEOPSTRUCT-wFunc fileop) FO_DELETE)
    (setf (LPSHFILEOPSTRUCT-pFrom fileop) filename)
    (setf (LPSHFILEOPSTRUCT-pTo fileop) 0)
    (setf (LPSHFILEOPSTRUCT-fFlags fileop) (logior FOF_ALLOWUNDO FOF_NOCONFIRMATION))
    (setf (LPSHFILEOPSTRUCT-fAnyOperationsAborted fileop) 0)
    (setf (LPSHFILEOPSTRUCT-hNameMappings fileop) 0)
    (setf (LPSHFILEOPSTRUCT-lpszProgressTitle fileop) 0)
    (SHFileOperation fileop)))

(in-package "editor")

;; filer-delete を少し変更
(defun filer-delete-api ()
  (long-operation
    (let ((marks (filer-get-mark-files)))
      (when (and marks
                 (if *filer-query-delete-precisely*
                     (filer-query-delete marks)
                     (yes-or-no-p "~A" (concat "選択されたファイルを削除しまっせ"
                                               (and *filer-delete-mask*
                                                    (filer-delete-mask-string *filer-delete-mask*
                                                                              "\n削除マスク: "))))))
        (filer-subscribe-to-reload (filer-get-directory) t)
        (let ((if-access-denied (if *filer-delete-read-only-files*
                                    :force :error))
              (delete-non-empty-directory
               *filer-delete-non-empty-directory*))
          (declare (special if-access-denied
                            delete-non-empty-directory))
;;;           (filer-do-delete marks))
          (win-user::filer-delete-api-1
           (mapcar #'(lambda (path)
                       (string-right-trim "\\" (map-slash-to-backslash path)))
                   marks)))
        (filer-subscribe-to-reload (filer-get-directory) t)
        (message "done.")))))
(define-key filer-keymap #\D 'filer-delete-api)

(in-package "user")

xyzzy ファイラで削除するかどうかの確認をするので
Windows のほうでは確認させないようにしたが、
そっちでも確認のダイアログを出したいなら

    (setf (LPSHFILEOPSTRUCT-fFlags fileop) (logior FOF_ALLOWUNDO FOF_NOCONFIRMATION))

の部分の FOF_NOCONFIRMATION を削除すればOK。


一応、単一または複数のファイル、ディレクトリで動作確認をした。
ファイル名に空白が入ってたり日本語が入ってたりしても
問題なく動いているみたい。
これで、大量のファイルを含むフォルダを削除しようとして
xyzzy が数分間使えなくなる、ということもなくなるはず。


それにしても、コンテキストメニューでできることをするだけなのに
こんなわけのわからないコードになるとは…。
これは xyzzy の問題じゃなくて MicrosoftAPI の問題なのかな。