avy icon indicating copy to clipboard operation
avy copied to clipboard

Inconsistent use of order of characters in avy-keys

Open jyp opened this issue 9 years ago • 13 comments

I've organised my avy-keys in such a way that the easiest characters to type are at the beginning of the list. This works well if jumping requires typing at most one character. Indeed, avy uses first the characters at the front of avy-keys.

However, when jumping requires two keystrokes, then for the first keystroke avy picks the characters at the end of the list. This is annoying because these will be the most difficult to type letters.

Would it be possible to consistently use the avy-keys in a front-first fashion?

jyp avatar Jul 14 '16 13:07 jyp

As far as I can see I'd be happy if the line

(let ((ks (copy-sequence keys))

in the function avy-tree would be replaced by

(let ((ks (copy-sequence (reverse keys)))

jyp avatar Jul 14 '16 13:07 jyp

The characters are consistent if there's a single window. When there are two windows, it depends on which one is active: in one case it's consistent in other it isn't.

I'll have to look, I always use avy in a single window, so I never notice these things.

abo-abo avatar Jul 14 '16 15:07 abo-abo

The issue that I describe refers to a single window. Let me try a more concrete description:

Let's say that my list of characters begins with f j and ends with . /, and that the list is 10 characters long. Let us now assume that jump with avy and I have only two occurences. These will be accessible by typing either f or j. Let us know assume that I have about 28. Then 10 of them will be accessible by typing . and another character; 10 of them will be accessible by typing / and another character; the rest with a single character.

That is not optimal. I would prefer that most two-character sequences contain f or j instead of / and .

jyp avatar Jul 24 '16 08:07 jyp

Please provide an example file and a list of steps to reproduce.

abo-abo avatar Jul 25 '16 07:07 abo-abo

Here is how I configure avy:

(use-package avy
  :ensure t :pin melpa-stable
  :commands avy-goto-char avy-goto-word-1
  :init
  (setq
   avy-keys
   '(
     ?t ?n ?s ?e ?r ?i ?o ?a  ;; 'power eight': can type them without any movement of the hand.
        ?w ?f ?l ?u ?y ?\; ?d ?h ?x ?c ?v ?m ?, ?. ;; close to home rows; no pinkes
        ?g ?b ?j ?k ?\' ;; diagonals, pinkie moves
        ?q ?z ?p ?\/ ;; pinkies away from the home row
        ;; ?4 ?7 ?3 ?8 ?9 ?2 ?5 ?1 ?6 ;; digits; in an order similar to the above 
        ;; shifted letters come here ...
        ?T ?N ?S ?E ?R ?I ?A
        ?W ?F ?L ?U ?Y ?\: ?D ?H ?X ?C ?V ?M ?< ?> ;; close to home rows; no pinkes
        ;; These are too similar to be practical in the "fast jumping" context: ?0 ?O
        )))

Note that I carefully put the easy to type characters first in the list (I am using a colemak layout). This works very well when there are few choices. If I have only two of them ivy will prompt me to type either t or n.

Let us use the configuration as an example file, for example by pasting it in a scratch buffer. Then eval:

(avy-goto-word-1 ??)

To try and jump to the right occurrence of the ? character.

Then I see the following:

avy-infelicity

The issue that I have is that, avy is using the least easy to type character (>) as a prefix. I'd much rather if it were using any of the 'easy to type' characters, ie. those at the beginning of the avy-keys list, for this purpose.

jyp avatar Oct 05 '16 18:10 jyp

The issue that I have is that, avy is using the least easy to type character (>) as a prefix. I'd much rather if it were using any of the 'easy to type' characters, ie. those at the beginning of the avy-keys list, for this purpose.

The assumption avy makes is that occurrences near the start are more desirable and should be given the shortest keys. You can see this very well in your screenshot: the first candidate gets the "best" key of all - the first entry on avy-keys; the second candidate gets the second best key etc.

By the point > starts to get used, it's the only key available as to be used as a prefix. This is a reasonable strategy: if you use up your best keys for prefixes, you'll have the worst keys left to point to the best candidates (the ones in the front).

If you don't like this algorithm, you can use your own by re-defining avy-tree, which is a very simple 20-lines function. For example, add two nreverse like this:

(defun avy-tree (lst keys)
  "Coerce LST into a balanced tree.
The degree of the tree is the length of KEYS.
KEYS are placed appropriately on internal nodes."
  (let ((len (length keys)))
    (cl-labels
        ((rd (ls)
           (let ((ln (length ls)))
             (if (< ln len)
                 (nreverse
                  (cl-pairlis keys
                              (mapcar (lambda (x) (cons 'leaf x)) ls)))
               (let ((ks (nreverse (copy-sequence keys)))
                     res)
                 (dolist (s (avy-subdiv ln len))
                   (push (cons (pop ks)
                               (if (eq s 1)
                                   (cons 'leaf (pop ls))
                                 (rd (avy-multipop ls s))))
                         res))
                 (nreverse res))))))
      (rd lst))))

abo-abo avatar Oct 06 '16 15:10 abo-abo

The assumption avy makes is that occurrences near the start are more desirable and should be given the shortest keys.

Thinking about it more, I think that this assumption is the real issue for me. Would it be possible for avy to use a more clever stategy there? Perhaps provide extra information (eg. distance to the cursor, active window, etc.) in the list of candidates so that candidates could be sorted by likelihood before avy-tree kicks in?

jyp avatar Oct 07 '16 06:10 jyp

PRs welcome, you can be as clever as you want.

I value consistency over cleverness though. The current approach is nothing if not consistent.

abo-abo avatar Oct 07 '16 07:10 abo-abo

Great. I'll do some hacking and see what I can do. In any case I'm not proposing the current approach, whose predictability has value, I agree.

On Fri, Oct 7, 2016 at 9:47 AM, Oleh Krehel [email protected] wrote:

PRs welcome, you can be as clever as you want.

I value consistency over cleverness though. The current approach is nothing if not consistent.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/abo-abo/avy/issues/164#issuecomment-252174945, or mute the thread https://github.com/notifications/unsubscribe-auth/AABsY_Z9vfA4q6IVO_b9BSqRJvt9DWIsks5qxfkdgaJpZM4JMbHF .

jyp avatar Oct 08 '16 10:10 jyp

This looks related to https://github.com/abo-abo/avy/issues/194, where both are about changing the behavior of avy-tree. @jyp did you get anywhere with this?

pheaver avatar May 24 '17 22:05 pheaver

@pheaver: indeed the aim is the same as #194. As far as I am concerned the issues could be merged.

As for the status: I tried to do some fancy things, but I have not been facing many files with many times the same character, so I've lost interest somewhat. Hacking avy-tree as suggested above is a good starting point for what you want to do I think.

jyp avatar May 25 '17 18:05 jyp

I modified the avy-tree function so it behaves the way I like. As you can see, the j, k, l, and f, d, s, a, they go first in the list. Unfortunatelly, we have to sacrifice some of them when we need to make them prefix keys when there are two many destinations. So, avy-tree now assigns a bit less convenient keys to single-key shortcuts, and the most convenient keys to the first in the combos.

Here's a screenshot: dropbox link.

(use-package avy
  :config

  (setq avy-keys
	(list
	 ?j ?k ?l
	 ?f ?d ?s ?a
	 ?u ?i ?o ?p
	 ?r ?e ?w ?q
	 ?h ?g
	 ?y ?t
	 ?m ?n
	 ?b ?v ?c ?x ?z
	 ))

  (global-set-key (kbd "s-;") 'avy-goto-char-2)
  (define-key isearch-mode-map (kbd "s-;") 'avy-isearch)

  (defun avy-subdiv (n b)
  "Distribute N in B terms in a balanced way."
  (let* ((p (1- (floor (+ (log n b) 1e-6))))
         (x1 (expt b p))
         (x2 (* b x1))
         (delta (- n x2))
         (n2 (/ delta (- x2 x1)))
         (n1 (- b n2 1)))
    (append
     (make-list n1 x1)
     (make-list n2 x2)   ; originally this goes last, but I'd like the heaviest subtree to be bound to the first key in the list
     (list
      (- n (* n1 x1) (* n2 x2)))
     )))

  (defun avy-tree (lst keys)
    "Coerce LST into a balanced tree.
     The degree of the tree is the length of KEYS.
     KEYS are placed appropriately on internal nodes."
    (let* ((len (length keys))
	   (order-fn (cdr (assq avy-command avy-orders-alist)))
	   (lst (if order-fn
		    (cl-sort lst #'< :key order-fn)
		  lst)))
      (cl-labels
	  ((rd (ls)
	       (let ((ln (length ls)))
		 (if (< ln len)
		     (cl-pairlis keys
				 (mapcar (lambda (x) (cons 'leaf x)) ls))
		   (let* ((ks (copy-sequence keys))
			  (subdiv (avy-subdiv ln len))
			  (number-of-ones (cl-count 1 subdiv))
			  (number-of-non-ones (- len number-of-ones))
			  res)
		     (dolist (s subdiv)
		       (push (cons (pop (if (eq s 1)
					    (nthcdr number-of-non-ones ks)
					  ks))
				   (if (eq s 1)
				       (cons 'leaf (pop ls))
				     (rd (avy-multipop ls s))))
			     res))
		     (nreverse res))))))
	(rd lst))))
  )

koddo avatar May 20 '20 23:05 koddo

Any reason why we are not using advice here?

  (defun my-avy-subdiv (n b)
    "Distribute N in B terms in a balanced way."
    (let* ((p (1- (floor (+ (log n b) 1e-6))))
           (x1 (expt b p))
           (x2 (* b x1))
           (delta (- n x2))
           (n2 (/ delta (- x2 x1)))
           (n1 (- b n2 1)))
      (append
       (make-list n1 x1)
       (make-list n2 x2)   ; originally this goes last, but I'd like the heaviest subtree to be bound to the first key in the list
       (list
        (- n (* n1 x1) (* n2 x2)))
       )))

  (defun my-avy-tree (lst keys)
    "Coerce LST into a balanced tree.
     The degree of the tree is the length of KEYS.
     KEYS are placed appropriately on internal nodes."
    (let* ((len (length keys))
	         (order-fn (cdr (assq avy-command avy-orders-alist)))
	         (lst (if order-fn
		                (cl-sort lst #'< :key order-fn)
		              lst)))
      (cl-labels
	        ((rd (ls)
	             (let ((ln (length ls)))
		             (if (< ln len)
		                 (cl-pairlis keys
				                         (mapcar (lambda (x) (cons 'leaf x)) ls))
		               (let* ((ks (copy-sequence keys))
			                    (subdiv (avy-subdiv ln len))
			                    (number-of-ones (cl-count 1 subdiv))
			                    (number-of-non-ones (- len number-of-ones))
			                    res)
		                 (dolist (s subdiv)
		                   (push (cons (pop (if (eq s 1)
					                                  (nthcdr number-of-non-ones ks)
					                                ks))
				                           (if (eq s 1)
				                               (cons 'leaf (pop ls))
				                             (rd (avy-multipop ls s))))
			                       res))
		                 (nreverse res))))))
	      (rd lst))))
(advice-add #'avy-subdiv :override #'my-avy-subdiv)
(advice-add #'avy-tree :override #'my-avy-tree)

hrehfeld avatar Jul 14 '21 12:07 hrehfeld