2

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?

jcubic
  • 61,973
  • 54
  • 229
  • 402
  • 6
    The sane way to do this is to use a `defun` instead if a `lambda`. Then removing it is trivial (especially if you might want to edit the body between runs. Then if you used a `lambda`, how do you find instances of both the old or the new definition? Etc). – tripleee Mar 26 '14 at 19:35
  • @triple's answer is the best one - s?he should post it as an answer, and you should accept it (IMHO). You should *not* be fiddling with adding extra functions to do what you are trying to do (remove specific lambda forms). Just do yourself a favor and give those anonymous functions **names**. This is a prime example of why one wants to name functions. Of course, if you have no control over the addition of such anonymous functions to a hook then you're out of luck. But typically you can just avoid doing that. – Drew Mar 26 '14 at 20:15
  • @Drew I will need to came up with a way to define 2 function names based on unix path, wich probably will break (like when path have whitespace or `'`), I don't think it's good idea to create custom function with a name in a macro and then assign that name to the hook, you have lambdas for that. – jcubic Mar 26 '14 at 20:41
  • I see. You did not mention any of that. Dunno why you need the name to be based on a path or whatever, but if you do, you do. – Drew Mar 26 '14 at 21:06
  • @Drew look at `project-specifics` macro, it add hook based on a path. if you want to add function instead of lambda you will need to name that function somehow, and the only way to recognize the function is name argument (which is a path), so you will need to convert that path to a function name. – jcubic Mar 27 '14 at 09:08
  • It's not hard to convert that `NAME` arg to a function name, regardless of whether it is a "path" (by which I assume you mean an absolute file name). Whether you want to do that or not is up to you. But it is quite doable. That's precisely the kind of thing that macros are good at. – Drew Mar 27 '14 at 13:47

3 Answers3

4

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.

sds
  • 58,617
  • 29
  • 161
  • 278
  • Big +1 for mentioning `.dir-locals.el`. It's surprising how little now this is, and how many people painfully implement their own hacks for project-specific variables. –  Mar 28 '14 at 09:47
3

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.

TheDude
  • 109
  • 5
1

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.

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • But what about if I want to have few projects. I think The code will need to keep them in AList or a hash for this to work. – jcubic Mar 27 '14 at 17:59
  • There was nothing like that in your original requirements; how would you pull that off with what you had? But yes, extending this with an alist would make it a lot more versatile without complicating it much. – tripleee Mar 27 '14 at 18:50
  • If you are satisfied with one action but want many projects, `string-match-p` takes a regex as its first argument. My docstrings are slightly off. So you could put `"projects/test\\|foo/bar"` to have it trigger in two projects at the same time. – tripleee Mar 27 '14 at 19:02
  • Fixed the docstrings now, and refactored a bit to reduce repeated code. – tripleee Mar 28 '14 at 09:56