Skip to content

Instantly share code, notes, and snippets.

@alphapapa
Last active December 21, 2022 15:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alphapapa/a3433c54a631b693ba9d to your computer and use it in GitHub Desktop.
Save alphapapa/a3433c54a631b693ba9d to your computer and use it in GitHub Desktop.
Complete multiple org-mode tags with helm
;;;;;; Fix Helm org tag completion
;; From Anders Johansson <https://groups.google.com/d/msg/emacs-helm/tA6cn6TUdRY/G1S3TIdzBwAJ>
;; This works great! He posted it on 3 Mar 2016, on a thread that was
;; started in Oct 2013. He also posted this message on 2 Apr 2014,
;; maybe an earlier attempt at a solution:
;; <http://article.gmane.org/gmane.emacs.orgmode/84495> I've just
;; tidied it up a bit and adjusted the prompt.
(add-to-list 'helm-completing-read-handlers-alist '(org-capture . aj/org-completing-read-tags))
(add-to-list 'helm-completing-read-handlers-alist '(org-set-tags . aj/org-completing-read-tags))
(defun aj/org-completing-read-tags (prompt coll pred req initial hist def inh)
(if (not (string= "Tags: " prompt))
;; Not a tags prompt. Use normal completion by calling
;; `org-icompleting-read' again without this function in
;; `helm-completing-read-handlers-alist'
(let ((helm-completing-read-handlers-alist (rassq-delete-all
'aj/org-completing-read-tags
helm-completing-read-handlers-alist)))
(org-icompleting-read prompt coll pred req initial hist def inh))
;; Tags prompt
(let* ((initial (and (stringp initial)
(not (string= initial ""))
initial))
(curr (when initial
(org-split-string initial ":")))
(table (org-uniquify
(mapcar 'car org-last-tags-completion-table)))
(table (if curr
;; Remove current tags from list
(cl-delete-if (lambda (x)
(member x curr))
table)
table))
(prompt (if initial
(concat "Tags " initial)
prompt)))
(concat initial (mapconcat 'identity
(nreverse (aj/helm-completing-read-multiple
prompt table pred nil nil hist def
t "Org tags" "*Helm org tags*" ":"))
":")))))
(defun aj/helm-completing-read-multiple (prompt choices
&optional predicate require-match initial-input hist def
inherit-input-method name buffer sentinel)
"Read multiple items with `helm-completing-read-default-1'. Reading stops
when the user enters SENTINEL. By default, SENTINEL is
\"*done*\". SENTINEL is disambiguated with clashing completions
by appending _ to SENTINEL until it becomes unique. So if there
are multiple values that look like SENTINEL, the one with the
most _ at the end is the actual sentinel value. See
documentation for `ido-completing-read' for details on the
other parameters."
(let ((sentinel (or sentinel "*done*"))
this-choice res done-reading)
;; Uniquify the SENTINEL value
(while (cl-find sentinel choices)
(setq sentinel (concat sentinel "_")))
(setq choices (cons sentinel choices))
;; Read choices
(while (not done-reading)
(setq this-choice (helm-completing-read-default-1 prompt choices
predicate require-match initial-input hist def
inherit-input-method name buffer nil t))
(if (equal this-choice sentinel)
(setq done-reading t)
(setq res (cons this-choice res))
(setq prompt (concat prompt this-choice ":"))))
res))
@robertsck
Copy link

Thanks for posting this - this works great! Quick question - is there an easy way to make this work on FILETAGS: property entries as well as headings? I noticed when I try to use it there I get a message that I'm not on a heading. I'm a beginner at elisp and couldn't find an obvious way to expand the function to handle that use case.

@clemera
Copy link

clemera commented Jul 18, 2020

I wonder why org-set-tags doesn't use completing-read-multiple itself.

@clemera
Copy link

clemera commented Jul 18, 2020

(defun org-set-tags-command-multiple (orig &optional arg)
  (cl-letf (((symbol-function #'completing-read)
             (lambda (prompt collection &optional predicate require-match initial-input
                             hist def inherit-input-method)
               (when initial-input
                 (setq initial-input
                       (replace-regexp-in-string
                        ":" ","
                        (replace-regexp-in-string
                         "\\`:" "" initial-input))))
               (let ((res (completing-read-multiple
                           prompt collection predicate require-match initial-input
                           hist def inherit-input-method)))
                 (mapconcat #'identity res ":")))))
    (let ((current-prefix-arg arg))
      (call-interactively orig))))

(advice-add #'org-set-tags-command :around #'org-set-tags-command-multiple)

@prasoon2211
Copy link

Thanks, works great @clemera

@alphapapa
Copy link
Author

@robertsck Oops, I didn't notice your comment for a few years. :) You would need to write a command to do that, which could use these tag completion functions to do so.

@clemera Thanks. If you haven't already, would you propose that on the Org mailing list?

@clemera
Copy link

clemera commented Oct 23, 2020

@alphapapa I have, it stopped here. I only use the simplest org features myself and only noticed this problem via a bug report so I wasn't motivated enough to proceed.

@alphapapa
Copy link
Author

@clemera Thanks. For my own use, I'm satisfied with the code in this gist, but maybe I'll look at the CRM version again sometime.

@darabi
Copy link

darabi commented Nov 7, 2020

As this is currently one of the top search results for 'org-mode helm select tag multiple', I'd like to point to

emacs-helm/helm-org#3

The mentioned fix works for me on GNU Emacs 27.0.50 and Org 9.3.6:

(require 'helm-org)
(add-to-list 'helm-completing-read-handlers-alist '(org-set-tags-command . helm-org-completing-read-tags))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment