jwiegley / use-package

A use-package declaration for simplifying your .emacs
https://jwiegley.github.io/use-package
GNU General Public License v3.0
4.4k stars 260 forks source link

add-hook syntax sugar #444

Closed nickserv closed 6 years ago

nickserv commented 7 years ago

I find use-package's bind-key macro (and the related :bind keyword) very helpful for cleaning up duplicate configuration. Hooks also tend to get out of hand on me, especially since I often need to add multiple functions to one hook, or add one function to multiple hooks. I think it would be great to have an add-hooks macro with a use-package keyword, which could work similarly to the binding feature.

I created a custom add-hooks macro (inspired by bind-keys) before I switched to use-package, which I'm still using to declare my hooks. It takes several cons pairs, where either the hook or function can be a single symbol or a list of symbols, in which case the pair will automatically apply to multiple functions and/or hooks. Here's an example:

(add-hooks
 ;; Programming modes
 (prog-mode-hook . (flyspell-prog-mode linum-mode rainbow-mode))

 ;; Text modes
 (text-mode-hook . (flyspell-mode auto-fill-mode))

 ;; Clean whitespace when saving.
 (before-save-hook . whitespace-cleanup)

 ;; Use Emmet to complete CSS and HTML.
 ((css-mode-hook sgml-mode-hook) . emmet-mode)

 ;; Use Tern for JS analysis.
 (js-mode-hook . tern-mode)

The use-package keyword could look something like this, taking either a cons cell or a list of cells:

(use-package emmet-mode
  :hooks
  ((css-mode-hook sgml-mode-hook) . emmet-mode))
joewreschnig commented 7 years ago

I was going to file a request for a related feature, which is that I can't find a way to combine autoloads and hooks safely, and I think this could potentially solve that too. (Unless it's already solvable - I only started using use-package yesterday.)

If I want an autoload of a package that only activates via a hook (for me, this is flycheck-mode and rainbow-mode), and add-hook within :init, then I have message spam every time the mode is activated if the package is not installed. But if I add-hook in :config, then it won't hook until I run the autoloading command explicitly.

With a special :hooks, use-package would have the information it needs to make the unloaded stub command quieter, or unhook it completely after the package isn't found.

nickserv commented 7 years ago

I set all my hooks under :config, and I don't seem to have the same hook activation issue. However, I do use :ensure to install all third party packages, I wonder if that's causing my hooks to work properly.

For example, here's my use-package declaration for rainbow-mode, which can successfully set up a hook even if the package wasn't installed previously:

(use-package rainbow-mode
  :ensure
  :config
  (add-hook 'prog-mode-hook 'rainbow-mode))

Are you basically suggesting that the :hooks keyword could set up hooks somewhere in the lifecycle similar to :config, but only if the package has been installed at that point? Sorry, I'm a bit confused about here since I'm relatively new to Emacs Lisp and I'm still grasping how use-package handles autoloads.

nickserv commented 7 years ago

In other news, my add-hooks package is now on MELPA and MELPA Stable.

I'm using it under :config to set up some of my more complex hooks, which is especially useful when a package introduces a minor mode which I want in multiple major modes. That being said, it would still be useful to have additional syntax sugar and assumptions for hook setup built into use-package (for example, using the package's name as the default FUNCTION for add-hook, similarly to :mode's assumptions).

joewreschnig commented 7 years ago

If I understand use-package's model, that works for you because nothing in that state makes it deferred. So it works, but you are also loading rainbow mode even if you never use it. If instead it's using one of the deferring features, e.g. :commands:

(use-package rainbow-mode
  :commands rainbow-mode
  :config
  (add-hook 'prog-mode-hook 'rainbow-mode))

It won't ever load, because :config won't run until one of the loading triggers - in this case, the command rainbow-mode - runs. Which it doesn't, because the hook also isn't added until it runs. But if I hook during :init, that's also no good because I don't know that the package will exist.

Giving higher-level control of hooking over to use-package would let it hook something during the same time as :init which reacts better if the package turns out not to exist once it first tries to trigger the load.

nickserv commented 7 years ago

Thanks for the explanation, that clears up some of my confusion about lazy loading.

I think the :hooks keyword should defer and autoload the package (similarly to :bind), adding hooks immediately if the package exists or is demanded. Adding hooks ahead of time shouldn't be a problem because add-hook can automatically initialize HOOK to nil when it doesn't exist yet. Hooks wouldn't be added when packages are missing, preventing missing package errors.

For syntax sugar, the FUNCTION could be optional, defaulting to the package's name (with -mode appended if it's not part of the package name and the variable exists), so in most cases you'd only have to define HOOK names. HOOK names could imply -mode and FUNCTION names could imply -hook or -mode-hook. HOOKs and FUNCTIONs could be symbol lists to imply multiplication and multiple pairs of them could be given in a list as well (just like add-hooks, if that's confusing please check its comments and docstrings). Depending on how fancy the syntax would be, it could depend on add-hooks and I could upstream some syntax that isn't specific to use-package (like the -hook and -mode stuff).

Example

(use-package flyspell
  :ensure
  :hooks (prog . flyspell-prog))

(use-package linum
  :hooks (prog))

(use-package rainbow-mode
  :ensure
  :hooks (prog))
joewreschnig commented 7 years ago

Appending -hook is fine; every normal hook should end in -hook and virtually every one does. But there are a lot of useful hooks that aren't mode-specific, and also a few common multi-modal ones like c-mode-common-hook, which don't end in -mode-hook.

Other deferring keywords like :mode, :interpreter, and :commands also require an explicit -mode when it doesn't match the package name. I think fighting against the repetition of -mode in Emacs code is probably a Sisyphean task, and would prefer it to stay explicit. It's always harder to disable implicit behavior when you don't want it, and harder to keep compatibility if you need to back it out because of other concerns.

In other words, I'd prefer

(use-package flyspell
  :hooks (prog-mode . flyspell-prog-mode))

(use-package linum
  :hooks (prog-mode . linum-mode))

(use-package rainbow-mode
  :hooks prog-mode)
nickserv commented 7 years ago

I agree, let's make only -hook implied in terms of naming hooks and functions. It's also easier to read since hook naming has a stronger standard and the hooks are always first, while the other assumptions were more ambiguous and would have some complicated edge cases.

I would still like the syntax to allow for multiple hooks in a pair, multiple functions in a pair, and multiple pairs. I think multiple pairs should have another pair of parens around them to mirror :bind's syntax for one or more bindings.

I plan on putting the implied -hook syntax in add-hooks, then I may tinker with with implementing a :hooks keyword that uses it. I'd like it to be similar to the bind-keys API which lets you use it in one large form that doesn't depend on use-package or in use-package's :bind keywords.

jwiegley commented 6 years ago

This is really nice, thanks for the idea (and reference implementation)! Now I can say:

(use-package abbrev
  :diminish
  :hook
  ((text-mode prog-mode erc-mode LaTeX-mode) . abbrev-mode)
  (expand-load
   . (lambda ()
       (add-hook 'expand-expand-hook 'indent-according-to-mode)
       (add-hook 'expand-jump-hook 'indent-according-to-mode)))
  :config
  (if (file-exists-p abbrev-file-name)
      (quietly-read-abbrev-file)))
4lex1v commented 6 years ago

Guess it's too late to contribute to the discussion, but maybe my case could be considered as a form of a future improvement. My config uses a very similar approach described here, but in an inverted manner. Having a modularized approach to emacs' configuration, I've added a keyword :hooks that receives a list of functions to call when a given mode is loaded, i.e say there's a configuration to work with the language of your choice, which requires a list of packages to be loaded, hence i have:

(use-package scala-mode
  :hooks (4lex1v/fix-scala-fonts
          hs-minor-mode
          hideshowvis-enable
          yas-minor-mode
          company-mode))

This approach looks cleaner to me, cause if there's a need to drop this config, there's only one place i need to change, rather then traverse all other configs removing the hook.

jwiegley commented 6 years ago

The exact syntax you quoted above now works, it's just called :hook instead of :hooks, for the same reason that it's :bind instead of :binds.

4lex1v commented 6 years ago

@jwiegley thanks for a fast response! Correct me if i'm wrong, but I believe given implementation (the :hook keyword) has a different semantics, having the definition from the example:

(use-package ace-jump-mode
  :hook scala-mode)

This would load ace-jump-mode when we launch scala-mode (desugared into (add-hook 'scala-mode-hook #'ace-jump-mode), while i believe the other way has its own convenience, it would look the other way:

(use-package scala-mode
  :hook ace-jump-mode)

Given the current implementation this would be desugared into (add-hook 'ace-jump-mode-hook #'scala-mode), which is not the intended way. Maybe i'm missing something and my understanding is incorrect. Does it make more sense to give a list of functions on the "hook" side, rather the other way around, or it's just a matter of taste?

jwiegley commented 6 years ago

If :hook had the reverse semantics, as you describe, how would you add to text-mode-hook or prog-mode-hook, which are far more common (at least, for me) than hooking one externally installed package into another.

jwiegley commented 6 years ago

Also, I tend to prefer that "more derived" packages reference the more basic ones, than the other way around. That is, counsel should refer to ivy, not ivy to counsel.

4lex1v commented 6 years ago

@jwiegley answering your question:

how would you add to text-mode-hook or prog-mode-hook, which are far more common

The approach i use - wrapping with a use-package, e.g for eshell. Haven't seen issues with this approach.

Also, I tend to prefer that "more derived" packages reference the more basic ones, than the other way around. That is, counsel should refer to ivy, not ivy to counsel.

I see the rational behind this, that's what i had initially. Current configuration strives towards a more modular approach, i.e major mode configuration with all minor mode dependencies, but it's a personal preference.

Anyway, thanks for taking your time considering my case 👍

jwiegley commented 6 years ago

Adding a private keyword, such as :call, would be trivial. If enough other people prefer that ordering, we could add it to use-package.

(use-package text-mode
  :call (flyspell-mode ace-jump-mode auto-fill-mode))

This would have the behavior that when text-mode is loaded (:defer is implicit here), those functions will be called.

joewreschnig commented 6 years ago

The approach i use - wrapping with a use-package, e.g for eshell. Haven't seen issues with this approach.

eshell is a package, and a feature, and a file, so everything "just works". package-install, load / require, and featurep all want the same identifier.

prog-mode is a feature and a file but not a package, and text-mode is merely a file. The former has some annoying edge cases when mixed with :ensure since package-install doesn't quite do the right thing. The latter is unaddressable. You'd need to hang it off simple (a feature) or emacs (a feature and a package), and then the shorthand doesn't work anymore anyway.

npostavs commented 6 years ago

(use-package "text-mode" ...) should work.

joewreschnig commented 6 years ago

It "works" but it might not do what you expect.

npostavs commented 6 years ago

If you have use-package-always-ensure set to t (or set it to t explicitly), you'll try to package-install it, eventually producing an error about not finding a package named "text-mode-".

Use :ensure nil.

Because no feature is provided, text-mode.el's contents will be re-evaluated every time.

Use :defer t.

dieggsy commented 6 years ago

@joewreschnig I'm not sure I follow. Eshell and text-mode are the same in this regard for me: both are features defined in some file that provides them (also can double check with (featurep text-mode), which returns t). I'd need to make sure :ensure is set to nil for both, because neither is available as a package on melpa or gnu elpa.

npostavs commented 6 years ago

(also can double check with (featurep text-mode), which returns t

That's new in Emacs 26 or so.

dieggsy commented 6 years ago

@npostavs ah, thanks for the clarification.

nickserv commented 6 years ago

@jwiegley Thanks for all this work, I finally got around to migrating from my add-hooks package to :hook and I love the syntax!

On a related note about add-hooks: When I originally created the package I was inspired by bind-key, and I wanted a similar library that could be used to define hooks the same way with or without using use-package (just like what bind-key does for global-set-key). I start implementing my own :hooks keyword with a similar syntax but using add-hooks internally (although I couldn't get it to work . at the time). My goal was to keep the syntax implementation in add-hooks so that it would be agnostic of the use-package codebase. This way any bugs or feature changes in use-package :hook syntax and the add-hooks function could be made once together for easier maintenance, and another alternative configuration management package could use the same hook resolution as use-package and add-hooks for modularity, just like bind-key. Do you think it would be worth it if I refactored the :hook keyword to use the add-hooks package internally (just like bind-key) as long as I can keep it back compatible with your current syntax?