Function that remove specific lambda from a hook i

2019-02-27 11:56发布

I found this macro, to run code for specific project path:

(defmacro project-specifics (name &rest body)
  `(progn
     (add-hook 'find-file-hook
             (lambda ()
               (when (string-match-p ,name (buffer-file-name))
                 ,@body)))
     (add-hook 'dired-after-readin-hook
             (lambda ()
               (when (string-match-p ,name (dired-current-directory))
                 ,@body)))))

and I use it:

(project-specifics "projects/test"
  (message "z"))

And I work on modification that will remove prevoius lambda from the hook, so far I have helper functions

(defun remove-lambda-helper (list matcher)
  (dolist (item list)
    (if (and (listp item) (eq (car item) 'lambda))
        (when (funcall matcher item)
          (message "found")
          (setq list (delete item list))))))

(defun remove-hook-name-lambda (name hook)
  (remove-lambda-helper hook
                        (lambda (body)
                          (equal (cadr (cadr (caddr body))) name))))

But when I call:

(remove-hook-name-lambda "projects/test" find-file-hook)

found is show up in *Messages* buffer but the lambda is not removed. What's wrong here?

3条回答
贼婆χ
2楼-- · 2019-02-27 12:37

Add the lambdas to the hooks and to your own hash table keyed by name. Then when you need to remove it, look up the lambda in the hash table and delq it from the hook.

About Elisp hash tables: http://www.gnu.org/software/emacs/manual/html_node/elisp/Hash-Tables.html

Btw (eq (car item) 'lambda) will fail when lexical-binding t.

查看更多
聊天终结者
3楼-- · 2019-02-27 12:38

The Problem

The reason is probably that the found object is the first in list, in which case (delete item list) returns (cdr list) instead of modifying its structure to preserve identity.

The important point here is that delete cannot guarantee that

(eq x (delete item x))
==> t

e.g. when item is the only element of x, delete will return nil which cannot be eq to the original cons.

The Solution

The solution is to return the new value of list from remove-lambda-helper by replacing

(dolist (item list) ...)

with

(dolist (item list list) ...)

and use it in remove-hook-name-lambda like in add-to-list:

(defun remove-hook-name-lambda (name hook-name)
  (set hook-name
       (remove-lambda-helper (symbol-value hook)
                             (lambda (body)
                               (equal (cadr (cadr (caddr body))) name)))))

The Final Remark

Adding lambdas to hooks is not a very good idea, especially if you want to remove them later. Note that your lambda removal test will fail if you happen to compile your code.

Lambdas also accumulate in the hook if you modify them, e.g., if you have

(add-hook 'my-hook (lambda () ...))

and then you modify the lambda and evaluate the add-hook form again, you will end up with two lambdas in the hook.

The much better solution is to use defun to define the functions:

(defmacro add-project-specifics (name &rest body)
  (let ((ffh (intern (concat name "-find-file-hook")))
        (darh (intern (concat name "-dired-after-readin-hook"))))
    `(progn
       (defun ,ffh ()
         (when (string-match-p ,name (buffer-file-name))
           ,@body))
       (add-hook 'find-file-hook ',ffh)
       (defun ,darh ()
         (when (string-match-p ,name (dired-current-directory))
           ,@body))
       (add-hook 'dired-after-readin-hook ',darh))))

(defmacro remove-project-specifics (name)
  (let ((ffh (intern (concat name "-find-file-hook")))
        (darh (intern (concat name "-dired-after-readin-hook"))))
    `(progn
       (remove-hook 'find-file-hook ',ffh)
       (unintern ',ffh nil)
       (remove-hook 'dired-after-readin-hook ',darh)
       (unintern ',darh nil))))

PS

Responding to the concern you expressed in a comment, special characters in symbols are okay as long as you quote them when dealing with the reader; since you are not going to do that - you will be only using add-project-specifics and remove-project-specifics - you should be fine with interning them.

The Best Solution to Your Actual Problem

Use Per-Directory Local Variables in .dir-locals.el.

查看更多
萌系小妹纸
4楼-- · 2019-02-27 12:43

I would like to propose a refactoring which allows you to define the hook once, then have it run the actual action only when some conditions are fulfilled.

(defvar project-specific-name-regex "projects/test"
  "*Regex to match on buffer name in order to trigger `project-specific-fun'.")
(defvar project-specific-fun (lambda () (message "z"))
  "*Lambda form to run when `project-specific-name-regex' triggers.")

(defsubst project-specific-trigger-maybe (name)
  (and project-specific-name-regex
       (stringp project-specific-name-regex)
       (string-match-p project-specific-name-regex name)
       project-specific-fun
       (funcall project-specific-fun) ) )
(defun project-specific-find-file ()
  "Hook run from `find-file-hook' to run `project-specific-fun' if it's set
and the buffer name matches `project-specific-name-regex'."
  (project-specific-trigger-maybe (buffer-file-name)) )
(defun project-specific-dired-after-readin ()
  "Hook run from `dired-after-readin-hook' to run `project-specific-fun'
if it's set and the directory name matches `project-specific-name-regex'."
  (project-specific-trigger-maybe (dired-current-directory)) )

(add-hook 'find-file-hook #'project-specific-find-file)
(add-hook 'dired-after-readin-hook #'project-specific-dired-after-readin)

So, just unset project-specific-name and/or project-specific-fun to disable the action from the hooks.

查看更多
登录 后发表回答