emacs-evil / evil-collection

A set of keybindings for evil-mode
GNU General Public License v3.0
1.22k stars 272 forks source link

Key translations #5

Closed jojojames closed 6 years ago

jojojames commented 7 years ago

https://github.com/jojojames/evil-integrations/issues/1#issuecomment-341961849 https://github.com/Ambrevar/evil-special-modes/issues/4

(12) @noctuid made some points on my repo: Ambrevar/evil-special-modes#4 I think the most important point is that we should provide for key translations so that users of non-QWERTY key maps can adapt easily (e.g. Dvorak, Colemak). I think I've seen a key translation system somewhere before, maybe in Evil.

I was looking at the expansion of

(evil-add-hjkl-bindings ag-mode-map 'normal
    "gg" #'evil-goto-first-line
    "gr" #'recompile
    "gj" #'compilation-next-error
    "gk" #'compilation-previous-error
    "\C-j" #'compilation-next-error
    "\C-k" #'compilation-previous-error
    "0" #'evil-digit-argument-or-evil-beginning-of-line
    "n" #'evil-search-next
    "N" #'evil-search-previous)
(evil-define-key 'normal ag-mode-map "h"
  (lookup-key evil-motion-state-map "h")
  "j"
  (lookup-key evil-motion-state-map "j")
  "k"
  (lookup-key evil-motion-state-map "k")
  "l"
  (lookup-key evil-motion-state-map "l")
  ":"
  (lookup-key evil-motion-state-map ":")
  "gg"
  (function evil-goto-first-line)
  "gr"
  (function recompile)
  "gj"
  (function compilation-next-error)
  "gk"
  (function compilation-previous-error)
  "
"
  (function compilation-next-error)
  ""
  (function compilation-previous-error)
  "0"
  (function evil-digit-argument-or-evil-beginning-of-line)
  "n"
  (function evil-search-next)
  "N"
  (function evil-search-previous))

lookup-key may be what we're looking for?

noctuid commented 7 years ago

I'm not sure how this would help. I don't know why evil-add-hjkl-bindings does this for sure (I'd have to look into it), but my guess is that it just wants to keep the hjkl bindings consistent with what's bound in the global motion state map. So, for example, if the user bound j and k to their visual line movement equivalents, and then used evil-add-hjkl-bindings afterwards, this change would be reflected. Maybe I'm wrong about the intention, but all this would do would be to give the wrong hjkl keybindings for someone who uses a different keyboard layout.

See my most recent suggestion in Ambrevar/evil-special-modes#4. Emacs allows binding a key to another key in a specific keymap (where the definition is (MAP . CHAR)). You couldn't swap keys like this if they were bound in the same keymap and pointing to the same keymap. However, you could potentially do something like this:

  1. Bind all keys in a newly created keymap
  2. Make that keymap the parent of the desired auxiliary keymap (which starts empty; e.g. (evil-get-auxiliary-keymap Info-mode-map 'state t t))
  3. The user can make these key to key definitions in the auxiliary map itself pointing to the parent map

It wouldn't be too hard to provide convenience functions to do this. Maybe there is a better way, but this is my initial idea.

jojojames commented 7 years ago

I'm curious if you wanted to drive this forward @noctuid.

I'm thinking the challenge here isn't "Can we do it?" but "Do we have time/energy to do it?"

I like the idea in supporting every type of user but 1. I don't use !Qwerty so testing it will be a big issue and 2. I'm a little wary of the complexity multiplier this might add.

If we can make some safe/big assumptions like QWERTY (or something very similar), most of the code doing the rebinding will be extremely simple/easy to read and will let anyone that needs to scratch an itch write their own binding and submit it. I'm not sure if supporting additional customization will add significant complexity bloat (it may not add any at all!)

Couple that with what I assume is an even smaller subset of users (maybe we can count them on one hand :)) that are doing Emacs -> Evil -> Colemak/Dvork/ETC seems tough.

We could use your help in this area if you were interested.

noctuid commented 7 years ago

My idea was not to support any other layout directly but to provide a means for users of other layouts to make the key translations themselves. This method would also be useful for QWERTY users who just want to globally replace certain keys. Ideally, this package shouldn't have to do much itself. If you'd be okay with it, I think it would be nice to provide the user with some helper functions for translation in the base package. I could write these and add information about them to the README.

The question is what the best way to do this is. @Somelauw pointed out in the other issue that you could just copy and replace a keymap after swapping keys (maybe that's what you were initially suggesting and I didn't understand correctly). I think a method that preserves the original keymap might be a better solution (think vim's noremap). For one, not all translations would need to be made at the same time. The user could also experiment with translations without having to reload the packages to try new ones (not as big a deal). This method would either require initially storing the original keymap in another variable for reference.

jojojames commented 7 years ago

If you'd be okay with it, I think it would be nice to provide the user with some helper functions for translation in the base package. I could write these and add information about them to the README.

That'd be awesome. Would most likely resolve this too.

https://github.com/jojojames/evil-collection/issues/2

@Somelauw pointed out in the other issue that you could just copy and replace a keymap after swapping keys (maybe that's what you were initially suggesting and I didn't understand correctly).

Yeah, something along those lines. I've seen some key remapping type code using lookup-key (not sure if it was the evilify macro or some other emacs package I've looked through once).

Somelauw commented 7 years ago

I think the following kinds of remappings can be useful.

Swapping keys For example, swap "h" and "n". Can also be described as two rotations: h -> n n -> h

Rotations For example t -> f f -> j j -> t

Preserving leader keys SPC -> S-SPC -> C-S-SPC So actions that would otherwise be bound to SPC, automatically get bound to the following key. The code of evil-evilified-state already does this.

Note: Regardless of how this is implemented, it would be best if this remapping system works with existing evil integrations (not just evil-collection), so that no changes are necessary to packages such as evil-magit and evil-org(-agenda).

noctuid commented 7 years ago

@Somelauw I added an experimental version to general.el if you want to try it. I'll make a pull request to add it here after testing it and getting some feedback. Here is an example call:

;; swap h and n in org-mode normal state
(general-translate-keys 'normal 'org-mode-map
  "h" "n"
  "n" "h")

This will work with any keymap (for non-evil keymaps, STATE can be nil). KEYMAP must be quoted. By default, it will store a backup of the keymap and do all lookups in the backup. This means that if you eval'd the previous example multiple times, the result would be the same. There is also a destructive option, so :destructive t will modify the keymap without making a backup. Calling the previous example multiple times with :destructive t would continuously swap and unswap "h" and "n".

@jojojames @Ambrevar The question is now how to best use something like this for all keymaps that evil-collection alters. This needs to be run after evil-collection creates its keybindings, so the current method would be something like:

(with-eval-after-load 'evil-proced
  (evil-collection-translate-keys 'motion 'proced-mode-map
    ...))

This isn't too bad, but there would be a lot of code duplication in the user config. That said, do you think it might make sense add some code at the end of each setup function to run some user function(s) (basically hooks)? It would pass the function a list of states/keymaps keybindings were created in so that the user could just define their translations in one place.

As an alternative solution, evil-define-key works fine even if the keymap does not exists (because of evil-delay). This means that with-eval-after-load is not necessary for keybindings created with evil-define-key. I'd need to check that code delayed by evil-delay will be executed in order (evil-define-key appends to the after-load-functions hook, so I'm guessing this is the case). If evil-collections allows running all evil-define-keys at once without eval-after-load based on a list of modes or keymaps (like mentioned in #7), the user could just iterate through that list after running the evil-collection setup function.

jojojames commented 7 years ago
;; swap h and n in org-mode normal state
(general-translate-keys 'normal 'org-mode-map
  "h" "n")

I wonder if we also want another function that achieves swaps instead of translations so the user can put in key pairs they want to swap.

;; swap h and n in org-mode normal state
(general-swap-keys 'normal 'org-mode-map
  "h" "n")

That said, do you think it might make sense add some code at the end of each setup function to run some user function(s) (basically hooks)? It would pass the function a list of states/keymaps keybindings were created in so that the user could just define their translations in one place.

I like this better than the alternative solution. What do you think @Ambrevar?

noctuid commented 7 years ago

I wonder if we also want another function that achieves swaps

The easiest way to do it would be to add a keyword argument :swap, but at that point it would just be shorter to specify both translations (though I could also just add a wrapper function ...-swap-keys that sets the keyword argument). If other people would prefer to have this, I can add it, but I personally wouldn't find it useful. I think it makes sense to just have one generic function that works for all cases.

jojojames commented 7 years ago

If other people would prefer to have this, I can add it, but I personally wouldn't find it useful. I think it makes sense to just have one generic function that works for all cases.

Fair enough.

noctuid commented 7 years ago

Looking at how evil-collection-init is written currently, I think the simplest thing to do would just be to call a user setup function. Something like this:

(defcustom evil-collection-user-setup-function #'ignore ...)
....
;; last line in evil-collection-init
(funcall evil-collection-user-setup-function m)

The only concern I'd have is non-standard keymap naming (which probably will never be an issue but is worth mentioning). It might make sense to add an additional function evil-collection-get-keymap-for-mode that will return the keymap symbol (or symbol-value) for the mode. Normally it would just add -map. This arguably might be useful even if non-standard keymap naming is never an issue for packages evil-collections deals with. Thoughts?

jojojames commented 7 years ago

Looking at how evil-collection-init is written currently, I think the simplest thing to do would just be to call a user setup function. Something like this:

Sounds good to me. Lets keep it simple.

The only concern I'd have is non-standard keymap naming (which probably will never be an issue but is worth mentioning). It might make sense to add an additional function evil-collection-get-keymap-for-mode that will return the keymap symbol (or symbol-value) for the mode.

Lets wait and see for a case to crop up before we fix it. It won't be too late then.

noctuid commented 6 years ago

I would like to try general-translate-keys or have it make it here at some point.

I've been pretty busy recently. I can probably make improvements and create a pull request next week.

jojojames commented 6 years ago

I've been pretty busy recently. I can probably make improvements and create a pull request next week.

Awesome. Sounds good.

YourFin commented 6 years ago

Why not, instead of directly mapping the functions to new key names, bind them to "middle-man" symbols that change on a per-mode basis? This would do a lot to:

That said, this would appear require that we have an always-active minor mode to overwrite existing keybinds that's loaded last, and is smart about not stepping on other modes' feet when it shouldn't; however I have a couple proposals to solve this foot-stepping issue:

I would be happy to implement either of these in my time off over the course of the next week with a bit of direction.

noctuid commented 6 years ago

Why not, instead of directly mapping the functions to new key names, bind them to "middle-man" symbols that change on a per-mode basis?

I don't understand this suggestion, but it sounds significantly more convoluted than the current implementation, and I'm not sure what the benefit would be. Could you provide a concrete example of the basic idea in emacs lisp and more description about what evil-collection would actually do and how the user would use the interface?

YourFin commented 6 years ago

Yeah, sorry that was rather poorly explained. The goal is to allow users to just do something more akin to (evil-collection-bind-key (kbd "h") 'evil-collection-down) or (evil-define-key 'normal 'evil-collection-map (kbd "h") 'evil-collection-down), and then have that automatically translate all of the keybinds given by this package. Using something likeevli-collection-jor simplyjwould work too instead ofevil-collection-down`; what I'm after is a more consistent customization interface with existing key-binding.

Which appears to be what you're working on already, after looking at your comment from Dec 8th again. Sorry, I apparently got rather enthralled last night in the details of implementation and didn't read your comment as carefully as I should have. If you want any help with your implementation I'd be happy to provide it, but no big deal either way.