abo-abo / swiper

Ivy - a generic completion frontend for Emacs, Swiper - isearch with an overview, and more. Oh, man!
https://oremacs.com/swiper/
2.31k stars 338 forks source link

Marking candidates #561

Closed jkitchin closed 5 years ago

jkitchin commented 8 years ago

What do you think about a feature like this. You can "mark" candidates and then the action can act on the marked list.

(defun ivy-reset-marked-candidates (orig-fun &rest args)
  (setq ivy-marked-candidates '()))

(advice-add 'ivy-read :before #'ivy-reset-marked-candidates)
;; (advice-add 'ivy-read  #'ivy-reset-marked-candidates)

(define-key ivy-minibuffer-map (kbd "C-<SPC>")
  (lambda (arg)
    "Add current candidate to `ivy-marked-candidates'.
If candidate is already in, remove it.
With prefix ARG show marked list"
    (interactive "P")
    (if arg
    (progn
      (setf (ivy-state-collection ivy-last)
        ivy-marked-candidates)
      (ivy--reset-state ivy-last))

      (let ((cand (or (assoc ivy--current (ivy-state-collection ivy-last))
              ivy--current)))
    (if (-contains? ivy-marked-candidates cand)
        ;; remove it
        (setq ivy-marked-candidates (-remove-item cand ivy-marked-candidates))
      (setq ivy-marked-candidates
        (append ivy-marked-candidates (list cand))))))))

;; Example usage use C-spc to "mark" some candidates, press enter to get insert
;; the comma-separated list. 
(ivy-read "select: " '("a" "b" "c" "d" "e")
      :action (lambda (x)
            (with-ivy-window 
              (insert (mapconcat 'identity
                     (or ivy-marked-candidates
                         (list x))
                     ", ")))))

;; a, c

;; desirable
;; 1. a face to show it is marked
;; 2. a way to restore the original list
abo-abo commented 8 years ago

It's possible to do this, but I've been avoiding it for now because there's no straightforward way to get an overview of your selection. Suppose you have 1000 cands, you've marked numbers 42, 128 and 999. And you have a window of 10 cands to examine your changes.

What I would like to get is something more like dired-mark and dired-unmark-backward. The mark status should be displayed in a column to the left of the candidate text. I don't like the idea of modifying face backgrounds or foregrounds, since those are already used usually.

One more problem are the key bindings. There aren't many good ones left in the minibuffer.

And a good place to start with the feature is for *ivy-occur* buffers:

My plan is first to get the feature working in *ivy-occur*, then maybe in the minibuffer. But first I have to find some time to do it. PRs welcome, of course.

jkitchin commented 8 years ago

I like the idea of the modal selection. In the minibuffer I guess you would have to toggle that.

What I have been doing is using C-spc to mark/unmark entries. C-, to show a list of the marked entries, C-. to restore the list of all entries, and M-Ret to act on all the marked entries. I mostly use this for selecting bibtex entries (from a list of about 1500) and inserting them all at once. Each time I run the command I reset the marked entries (although resuming doesn't do it yet.) You can check out an implementation here: https://github.com/jkitchin/org-ref/blob/master/org-ref-ivy-cite.el#L311 if you are interested.

I don't have a way to mark entries in the minibuffer yet.

jkitchin commented 8 years ago

Here is a variation that shows which entries are marked:

#+BEGIN_SRC emacs-lisp
(defvar ivy-marked-candidates '() "List of marked candidates")

(defun ivy-mark-candidate ()
  (interactive)
  (let ((cand (or (assoc ivy--current (ivy-state-collection ivy-last))
          ivy--current)))
    (if (-contains? ivy-marked-candidates cand)
    ;; remove it from the marked list
    (setq ivy-marked-candidates
          (-remove-item cand ivy-marked-candidates))

      ;; add to list
      (setq ivy-marked-candidates
        (append ivy-marked-candidates (list cand))))))

(defun ivy-marked-transformer (s)
  (if (-contains? ivy-marked-candidates s)
      (concat "M|" s)
    (concat " |" s)))

(ivy-set-display-transformer
 'testf
 'ivy-marked-transformer)

(define-key ivy-minibuffer-map (kbd "C-<SPC>")
  'ivy-mark-candidate)

(defun testf ()
  (interactive)
  (setq ivy-marked-candidates '())
  (ivy-read "select: " '("a" "b" "c" "d" "e")
            :caller 'testf
        :action (lambda (x)
              (with-ivy-window 
            (insert (mapconcat 'identity
                       (or ivy-marked-candidates
                           (list x))
                       ", "))))))
#+END_SRC

I was hoping you could just put ^M in the selection to show only the marked entries, but it didn't work.

abo-abo commented 8 years ago

I was hoping you could just put ^M in the selection to show only the marked entries, but it didn't work.

Here's how to make it work:

(defvar ivy-marked-candidates nil
  "List of marked candidates")

(defun ivy-mark-candidate ()
  (interactive)
  (let ((cand ivy--current))
    (if (member cand ivy-marked-candidates)
        (progn
          (setq ivy-marked-candidates
                (delete cand ivy-marked-candidates))
          (setcar (member ivy--current (ivy-state-collection ivy-last))
                  (setf (nth ivy--index ivy--old-cands) (substring cand 2))))
      (setcar (member ivy--current (ivy-state-collection ivy-last))
              (setq cand (setf (nth ivy--index ivy--old-cands) (concat "M|" cand))))
      (setq ivy-marked-candidates
            (append ivy-marked-candidates (list cand))))))

(define-key ivy-minibuffer-map (kbd "C-<SPC>") 'ivy-mark-candidate)

(defun testf ()
  (interactive)
  (setq ivy-marked-candidates '())
  (ivy-read "select: " (mapcar #'substring-no-properties
                               '("a" "b" "c" "d" "e"))
            :caller 'testf
            :action
            (lambda (x)
              (with-ivy-window
                (insert (mapconcat (lambda (s)
                                     (if (string-match "^M|" s)
                                         (substring s 2)
                                       s))
                                   (or ivy-marked-candidates
                                       (list x))
                                   ", "))))))

Note here both ivy--all-cands (whole collection) and ivy--old-cands (filtered collection) need to be updated.

jkitchin commented 8 years ago

Interesting, thanks! I am traveling until next week. I look forward to trying it out when I get back.

On Friday, June 17, 2016, Oleh Krehel notifications@github.com wrote:

I was hoping you could just put ^M in the selection to show only the marked entries, but it didn't work.

Here's how to make it work:

(defvar ivy-marked-candidates nil "List of marked candidates")

(defun ivy-mark-candidate () (interactive) (let ((cand ivy--current)) (if (member cand ivy-marked-candidates) (progn (setq ivy-marked-candidates (delete cand ivy-marked-candidates)) (setcar (member ivy--current (ivy-state-collection ivy-last)) (setf (nth ivy--index ivy--old-cands) (substring cand 2)))) (setcar (member ivy--current (ivy-state-collection ivy-last)) (setq cand (setf (nth ivy--index ivy--old-cands) (concat "M|" cand)))) (setq ivy-marked-candidates (append ivy-marked-candidates (list cand))))))

(define-key ivy-minibuffer-map (kbd "C-") 'ivy-mark-candidate)

(defun testf () (interactive) (setq ivy-marked-candidates '()) (ivy-read "select: " (mapcar #'substring-no-properties '("a" "b" "c" "d" "e")) :caller 'testf :action (lambda (x) (with-ivy-window (insert (mapconcat (lambda (s) (if (string-match "^M|" s) (substring s 2) s)) (or ivy-marked-candidates (list x)) ", "))))))

Note here both ivy--all-cands (whole collection) and ivy--old-cands (filtered collection) need to be updated.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/abo-abo/swiper/issues/561#issuecomment-226800593, or mute the thread https://github.com/notifications/unsubscribe/ABiRVgNIdGJB9qRqkTZZsBYPsjcDpJdUks5qMrzzgaJpZM4I2n0c .

John


Professor John Kitchin Doherty Hall A207F Department of Chemical Engineering Carnegie Mellon University Pittsburgh, PA 15213 412-268-7803 @johnkitchin http://kitchingroup.cheme.cmu.edu

jkitchin commented 8 years ago

That is a beautiful solution! Thanks for showing it to me.

CeleritasCelery commented 6 years ago

It looks like ivy--current is no longer used in the latest version of ivy. Is there a replacement for this?

abo-abo commented 6 years ago

Is there a replacement for this?

(ivy-state-current ivy-last)

dustinlacewell commented 5 years ago

Someone gonna package this?!

abo-abo commented 5 years ago

@jkitchin I've finally integrated your idea into ivy. It's not fully the original one, some adjustments may be necessary.

But with no additional config it's now possible to e.g.:

After this, to reopen them (assuming recentf and virtual buffers):

The disadvantage is that currently there's no way to write a special action function that is called once for 5 candidates, instead we call an action function for each one candidate 5 times. The advantage is of course that we don't have to adapt the old action functions.

jkitchin commented 5 years ago

Awesome, thanks! Here are a few examples I made playing around with it.

(define-key ivy-minibuffer-map (kbd "C-<SPC>") 'ivy-mark)
(let ((candidates '(("1" . 1) ("2" . 2) ("3" . 3) ("4" . 4))))
  (ivy-read "choice: " candidates
        :action
        (lambda (x)
          (message-box
           "%S"
           (cdr (assoc x candidates))))))

and then mark say 1, and 3, and press enter then you will see two sequential message boxes, with a 1 then a 3, i.e. the action is called sequentially on the marked candidates in the order they were marked.

Supposing you want to select a few candidates, and then insert them at the current point as a comma separated list, then it might look like this:

(let ((candidates '(("1" . 1) ("2" . 2) ("3" . 3) ("4" . 4))))
  (ivy-read "choice: " candidates
        :action
        (lambda (x)
          (with-ivy-window
        (if (looking-back " " 1)
            (insert x)
          (insert (concat "," x)))))))

and if you want the accumulated list of marked candidates for some other purpose (e.g. sorting, summing, etc) it seems like you need to do something like this:

(let ((candidates '(("1" . 1) ("2" . 2) ("3" . 3) ("4" . 4)))
      (accumulated '()))
  (ivy-read "choice: " candidates
        :action
        (lambda (x)
          (push x accumulated)))
  (message-box "%s" accumulated))

Since the action is called on each marked candidate, it doesn't seem like there is a way to work directly on the list of marked candidates from the action. The way helm does this is the action is just called once with the current candidate (whether there are marked ones or not) and then in the action you decide whether to use the current candidate or the marked candidate list. Does that make sense here too?

Is that what you had in mind for this?

abo-abo commented 5 years ago

I had this in mind:

(let ((candidates '(("1" . 1) ("2" . 2) ("3" . 3) ("4" . 4))))
  (ivy-read "choice: " candidates
            :action 'insert))

Press C-x C-e C-o mjmd. Candidates "1" and "3" are selected, so insert is called twice, and "13" is inserted into the buffer.

It doesn't matter if the candidates start with > (which is customizable anyway), an extra > will be added and stripped away before the :action is called.

I was thinking yesterday evening about the "combiner" action, i.e. the one that is called once for 5 candidates, instead of calling :action 5 times for each candidate.

Here's how it works:

(defun my-insert-action (x &optional lst)
  (if lst
      (insert (mapconcat #'identity lst ", "))
    (insert x)))

(defun my-test ()
  (interactive)
  (let ((candidates '("1" "2" "3" "4")))
    (ivy-read "choice: " candidates
              :action #'my-insert-action)))

Now you can call my-test, C-o td to insert 1, 2, 3, 4.