Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Marking candidates #561

Closed
jkitchin opened this issue Jun 15, 2016 · 12 comments
Closed

Marking candidates #561

jkitchin opened this issue Jun 15, 2016 · 12 comments

Comments

@jkitchin
Copy link
Contributor

@jkitchin jkitchin commented Jun 15, 2016

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
Copy link
Owner

@abo-abo abo-abo commented Jun 16, 2016

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:

  • They have modal bindings like dired, so binding m, u, U and DEL is fine.
  • They have a huge window of cands, as big as your screen.

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
Copy link
Contributor Author

@jkitchin jkitchin commented Jun 17, 2016

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
Copy link
Contributor Author

@jkitchin jkitchin commented Jun 17, 2016

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
Copy link
Owner

@abo-abo abo-abo commented Jun 17, 2016

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
Copy link
Contributor Author

@jkitchin jkitchin commented Jun 17, 2016

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
#561 (comment), 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
Copy link
Contributor Author

@jkitchin jkitchin commented Jul 8, 2016

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

@CeleritasCelery
Copy link
Contributor

@CeleritasCelery CeleritasCelery commented May 4, 2018

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

@abo-abo
Copy link
Owner

@abo-abo abo-abo commented May 8, 2018

Is there a replacement for this?

(ivy-state-current ivy-last)

@dustinlacewell
Copy link

@dustinlacewell dustinlacewell commented Jan 5, 2019

Someone gonna package this?!

@abo-abo abo-abo closed this in 28e88ab Feb 6, 2019
@abo-abo
Copy link
Owner

@abo-abo abo-abo commented Feb 6, 2019

@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.:

  • call ivy-switch-buffer
  • select org$
  • press C-o t to mark all selected candidates
  • press rk to kill all those buffers

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

  • call ivy-switch-buffer
  • select org$
  • press C-o t to mark all selected candidates
  • press d to open all 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
Copy link
Contributor Author

@jkitchin jkitchin commented Feb 7, 2019

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
Copy link
Owner

@abo-abo abo-abo commented Feb 7, 2019

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.

abo-abo added a commit that referenced this issue Feb 7, 2019
abo-abo added a commit that referenced this issue May 20, 2019
* ivy.el (ivy-call): The second arg in `ivy-action' must be called
  "marked-candidades" in order to be recognized. This is to allow the
  built-in functions such as `find-file-other-window' to be re-used as
  actions. The function in question, `find-file-other-window', already
  has 2 arguments, so it was mistaken for an action with the
  marked-candidates logic.
(ivy-mark): Document `marked-candidates'.

Re #561
Fixes #2068
abo-abo added a commit that referenced this issue May 22, 2019
Re #2068
Re #561

Example:

    (defun test ()
      (interactive)
      (ivy-read "test:" '("a" "b" "c")
                :action
                (lambda (x) (insert ":" x ":\n"))
                :multi-action
                (lambda (lst)
                  (insert (mapconcat #'identity lst ":") "\n"))))
astoff added a commit to astoff/swiper that referenced this issue Jan 1, 2021
These key bindings are now available in the "C-o" hydra.

* ivy.el (ivy-marked-candidates):
(ivy-mark-prefix): New defvar.
(ivy-call): Use `ivy-marked-candidates' when non-nil.
(ivy-read): Reset `ivy-marked-candidates' to nil.
(ivy-mark):
(ivy-unmark):
(ivy-unmark-backward):
(ivy-toggle-marks): New command.

Fixes abo-abo#561
astoff added a commit to astoff/swiper that referenced this issue Jan 1, 2021
astoff added a commit to astoff/swiper that referenced this issue Jan 1, 2021
* ivy.el (ivy-call): The second arg in `ivy-action' must be called
  "marked-candidades" in order to be recognized. This is to allow the
  built-in functions such as `find-file-other-window' to be re-used as
  actions. The function in question, `find-file-other-window', already
  has 2 arguments, so it was mistaken for an action with the
  marked-candidates logic.
(ivy-mark): Document `marked-candidates'.

Re abo-abo#561
Fixes abo-abo#2068
astoff added a commit to astoff/swiper that referenced this issue Jan 1, 2021
Re abo-abo#2068
Re abo-abo#561

Example:

    (defun test ()
      (interactive)
      (ivy-read "test:" '("a" "b" "c")
                :action
                (lambda (x) (insert ":" x ":\n"))
                :multi-action
                (lambda (lst)
                  (insert (mapconcat #'identity lst ":") "\n"))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants