From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by olra.theworths.org (Postfix) with ESMTP id 98606429E4C for ; Sun, 29 Jan 2012 14:58:02 -0800 (PST) X-Virus-Scanned: Debian amavisd-new at olra.theworths.org X-Spam-Flag: NO X-Spam-Score: -0.7 X-Spam-Level: X-Spam-Status: No, score=-0.7 tagged_above=-999 required=5 tests=[RCVD_IN_DNSWL_LOW=-0.7] autolearn=disabled Received: from olra.theworths.org ([127.0.0.1]) by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id xZymCiIjACJx for ; Sun, 29 Jan 2012 14:58:01 -0800 (PST) Received: from dmz-mailsec-scanner-4.mit.edu (DMZ-MAILSEC-SCANNER-4.MIT.EDU [18.9.25.15]) by olra.theworths.org (Postfix) with ESMTP id D4D7F431E64 for ; Sun, 29 Jan 2012 14:58:00 -0800 (PST) X-AuditID: 1209190f-b7f8a6d000000914-14-4f25cef8cadb Received: from mailhub-auth-1.mit.edu ( [18.9.21.35]) by dmz-mailsec-scanner-4.mit.edu (Symantec Messaging Gateway) with SMTP id 7D.D6.02324.8FEC52F4; Sun, 29 Jan 2012 17:58:00 -0500 (EST) Received: from outgoing.mit.edu (OUTGOING-AUTH.MIT.EDU [18.7.22.103]) by mailhub-auth-1.mit.edu (8.13.8/8.9.2) with ESMTP id q0TMvxIA014616; Sun, 29 Jan 2012 17:58:00 -0500 Received: from awakening.csail.mit.edu (awakening.csail.mit.edu [18.26.4.91]) (authenticated bits=0) (User authenticated as amdragon@ATHENA.MIT.EDU) by outgoing.mit.edu (8.13.6/8.12.4) with ESMTP id q0TMvwHX022164 (version=TLSv1/SSLv3 cipher=AES256-SHA bits=256 verify=NOT); Sun, 29 Jan 2012 17:57:59 -0500 (EST) Received: from amthrax by awakening.csail.mit.edu with local (Exim 4.77) (envelope-from ) id 1RrdgM-00007O-Hb; Sun, 29 Jan 2012 17:57:10 -0500 Date: Sun, 29 Jan 2012 17:57:10 -0500 From: Austin Clements To: Dmitry Kurochkin Subject: Re: [PATCH 3/6] emacs: make "+" and "-" tagging operations more robust Message-ID: <20120129225710.GG17991@mit.edu> References: <1327725684-5887-1-git-send-email-dmitry.kurochkin@gmail.com> <1327725684-5887-3-git-send-email-dmitry.kurochkin@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <1327725684-5887-3-git-send-email-dmitry.kurochkin@gmail.com> User-Agent: Mutt/1.5.21 (2010-09-15) X-Brightmail-Tracker: H4sIAAAAAAAAA+NgFprEKsWRmVeSWpSXmKPExsUixCmqrPvjnKq/wesjPBZXt/azW1y/OZPZ gclj56y77B7PVt1iDmCK4rJJSc3JLEst0rdL4Mp4f3Quc8HMXsaKnt0X2BoYfxZ2MXJySAiY SKx8fIoJwhaTuHBvPVsXIxeHkMA+Rol1p2+yQjgbGCU6Hx5ih3BOMkns29zDBOEsYZRY/fAu M0g/i4CqxMaz6xlBbDYBDYlt+5eD2SIChhK3Lr4Cq2EWkJb49rsZbJ+wQKBE1+W1LCA2r4CO xJ6t0xkhhnYySsyd/QIqIShxcuYTFohmLYkb/14CNXOADVr+jwMkzCngJXFnzyJ2EFtUQEVi ysltbBMYhWYh6Z6FpHsWQvcCRuZVjLIpuVW6uYmZOcWpybrFyYl5ealFuiZ6uZkleqkppZsY wcEtyb+D8dtBpUOMAhyMSjy8J5aq+AuxJpYVV+YeYpTkYFIS5W05o+ovxJeUn1KZkVicEV9U mpNafIhRgoNZSYR3zjKgHG9KYmVValE+TEqag0VJnFdN652fkEB6YklqdmpqQWoRTFaGg0NJ gjcPGMVCgkWp6akVaZk5JQhpJg5OkOE8QMN7QGp4iwsSc4sz0yHypxh1Od7O33+eUYglLz8v VUqcNxqkSACkKKM0D24OLCm9YhQHekuYNxKkigeY0OAmvQJawgS05DkD2JKSRISUVAOjdWMZ 177jsb8vzs8+ctXyvpi80ObOVV3icqyFdcK/hesjtHef7PqUFvtx/lp930mz1pw4Z/vPR917 zpwDleldHrrLW6JNmufd+1Dr1PzFYuOkCUL3bKT4avk9+DfIsMkpaCjUzItkMunq3z/7Qrlh t5b+wfVXCuZmZ/X3vAhi7f215E5Cp4USS3FGoqEWc1FxIgC9rLRQJQMAAA== Cc: notmuch@notmuchmail.org X-BeenThere: notmuch@notmuchmail.org X-Mailman-Version: 2.1.13 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Sun, 29 Jan 2012 22:58:02 -0000 I'm looking forward to having this. I think it'll streamline tagging operations. Quoth Dmitry Kurochkin on Jan 28 at 8:41 am: > Before the change, "+" and "-" tagging operations in notmuch-search > and notmuch-show views accepted only a single tag. The patch makes > them use the recently added `notmuch-select-tags-with-completion' > function, which allows to enter multiple tags with "+" and "-" > prefixes. So after the change, "+" and "-" bindings allow to both add > and remove multiple tags. The only difference between "+" and "-" is > the minibuffer initial input ("+" and "-" respectively). This patch was a little difficult to review because it was largish and the diff happened to contain a bunch of forward references. If it's convenient (don't bother if it's not), could you divide up the next version a little? Something simple like sending the show changes as a separate patch would probably make it a lot easier. > --- > emacs/notmuch-show.el | 65 +++++++------------ > emacs/notmuch.el | 165 +++++++++++++++++++++++++------------------------ > 2 files changed, 107 insertions(+), 123 deletions(-) > > diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el > index 84ac624..03eadfb 100644 > --- a/emacs/notmuch-show.el > +++ b/emacs/notmuch-show.el > @@ -38,8 +38,9 @@ > > (declare-function notmuch-call-notmuch-process "notmuch" (&rest args)) > (declare-function notmuch-fontify-headers "notmuch" nil) > -(declare-function notmuch-select-tag-with-completion "notmuch" (prompt &rest search-terms)) > +(declare-function notmuch-select-tags-with-completion "notmuch" (&optional initial-input &rest search-terms)) > (declare-function notmuch-search-show-thread "notmuch" nil) > +(declare-function notmuch-update-tags "notmuch" (current-tags changed-tags)) > > (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date") > "Headers that should be shown in a message, in this order. > @@ -1267,7 +1268,7 @@ Some useful entries are: > > (defun notmuch-show-mark-read () > "Mark the current message as read." > - (notmuch-show-remove-tag "unread")) > + (notmuch-show-tag-message "-unread")) > > ;; Functions for getting attributes of several messages in the current > ;; thread. > @@ -1470,51 +1471,33 @@ than only the current message." > (message (format "Command '%s' exited abnormally with code %d" > shell-command exit-code)))))))) > > -(defun notmuch-show-add-tags-worker (current-tags add-tags) > - "Add to `current-tags' with any tags from `add-tags' not > -currently present and return the result." > - (let ((result-tags (copy-sequence current-tags))) > - (mapc (lambda (add-tag) > - (unless (member add-tag current-tags) > - (setq result-tags (push add-tag result-tags)))) > - add-tags) > - (sort result-tags 'string<))) > - > -(defun notmuch-show-del-tags-worker (current-tags del-tags) > - "Remove any tags in `del-tags' from `current-tags' and return > -the result." > - (let ((result-tags (copy-sequence current-tags))) > - (mapc (lambda (del-tag) > - (setq result-tags (delete del-tag result-tags))) > - del-tags) > - result-tags)) > - > -(defun notmuch-show-add-tag (&rest toadd) > - "Add a tag to the current message." > - (interactive > - (list (notmuch-select-tag-with-completion "Tag to add: "))) > +(defun notmuch-show-tag-message (&rest changed-tags) > + "Change tags for the current message. > > +`Changed-tags' is a list of tag operations for \"notmuch tag\", > +i.e. a list of tags to change with '+' and '-' prefixes." Ticks in a docstring indicate functions (and will be hyperlinked as such by describe-function). Typically, argument names are indicated by writing them in all caps. Also, it probably makes more sense to reference `notmuch-tag' than "notmuch tag", since this is Lisp land (and, since that will be helpfully hyperlinked, you probably don't need to explain changed-tags here). > (let* ((current-tags (notmuch-show-get-tags)) > - (new-tags (notmuch-show-add-tags-worker current-tags toadd))) > - > + (new-tags (notmuch-update-tags current-tags changed-tags))) > (unless (equal current-tags new-tags) > - (apply 'notmuch-tag (notmuch-show-get-message-id) > - (mapcar (lambda (s) (concat "+" s)) toadd)) > + (apply 'notmuch-tag (notmuch-show-get-message-id) changed-tags) > (notmuch-show-set-tags new-tags)))) > > -(defun notmuch-show-remove-tag (&rest toremove) > - "Remove a tag from the current message." > - (interactive > - (list (notmuch-select-tag-with-completion > - "Tag to remove: " (notmuch-show-get-message-id)))) > +(defun notmuch-show-tag (&optional initial-input) > + "Change tags for the current message, read input from the minibuffer." > + (interactive) > + (let ((changed-tags (notmuch-select-tags-with-completion > + initial-input (notmuch-show-get-message-id)))) > + (apply 'notmuch-show-tag-message changed-tags))) > > - (let* ((current-tags (notmuch-show-get-tags)) > - (new-tags (notmuch-show-del-tags-worker current-tags toremove))) > +(defun notmuch-show-add-tag () > + "Same as `notmuch-show-tag' but sets initial input to '+'." > + (interactive) > + (notmuch-show-tag "+")) > > - (unless (equal current-tags new-tags) > - (apply 'notmuch-tag (notmuch-show-get-message-id) > - (mapcar (lambda (s) (concat "-" s)) toremove)) > - (notmuch-show-set-tags new-tags)))) > +(defun notmuch-show-remove-tag () > + "Same as `notmuch-show-tag' but sets initial input to '-'." > + (interactive) > + (notmuch-show-tag "-")) Should notmuch-show-{add,remove}-tag be considered public functions? Previously, they were amenable to creating bindings for adding or removing individual tags, and I believe people have done this. If we're okay with breaking backward-compatibility, there should at least be a NEWS item explaining how to convert such custom bindings to use notmuch-show-tag-message. > > (defun notmuch-show-toggle-headers () > "Toggle the visibility of the current message headers." > @@ -1559,7 +1542,7 @@ argument, hide all of the messages." > (defun notmuch-show-archive-thread-internal (show-next) > ;; Remove the tag from the current set of messages. > (goto-char (point-min)) > - (loop do (notmuch-show-remove-tag "inbox") > + (loop do (notmuch-show-tag-message "-inbox") > until (not (notmuch-show-goto-message-next))) > ;; Move to the next item in the search results, if any. > (let ((parent-buffer notmuch-show-parent-buffer)) > diff --git a/emacs/notmuch.el b/emacs/notmuch.el > index ff46617..24b0ea3 100644 > --- a/emacs/notmuch.el > +++ b/emacs/notmuch.el > @@ -76,38 +76,56 @@ For example: > (defvar notmuch-query-history nil > "Variable to store minibuffer history for notmuch queries") > > -(defun notmuch-tag-completions (&optional prefixes search-terms) > - (let ((tag-list > - (split-string > - (with-output-to-string > - (with-current-buffer standard-output > - (apply 'call-process notmuch-command nil t > - nil "search-tags" search-terms))) > - "\n+" t))) > - (if (null prefixes) > - tag-list > - (apply #'append > - (mapcar (lambda (tag) > - (mapcar (lambda (prefix) > - (concat prefix tag)) prefixes)) > - tag-list))))) > +(defun notmuch-tag-completions (&optional search-terms) > + (split-string > + (with-output-to-string > + (with-current-buffer standard-output > + (apply 'call-process notmuch-command nil t > + nil "search-tags" search-terms))) > + "\n+" t)) > > (defun notmuch-select-tag-with-completion (prompt &rest search-terms) > - (let ((tag-list (notmuch-tag-completions nil search-terms))) > + (let ((tag-list (notmuch-tag-completions search-terms))) > (completing-read prompt tag-list))) > > -(defun notmuch-select-tags-with-completion (prompt &optional prefixes &rest search-terms) > - (let ((tag-list (notmuch-tag-completions prefixes search-terms)) > - (crm-separator " ") > - ;; By default, space is bound to "complete word" function. > - ;; Re-bind it to insert a space instead. Note that > - ;; still does the completion. > - (crm-local-completion-map > - (let ((map (make-sparse-keymap))) > - (set-keymap-parent map crm-local-completion-map) > - (define-key map " " 'self-insert-command) > - map))) > - (delete "" (completing-read-multiple prompt tag-list)))) > +(defun notmuch-select-tags-with-completion (&optional initial-input &rest search-terms) I don't know if notmuch-select-tags-with-completion is the right name for this now that it hard-codes the +/- prefixes (which seems like the right thing to do, BTW). Maybe notmuch-read-tags-add-remove? > + (let* ((add-tag-list (mapcar (apply-partially 'concat "+") > + (notmuch-tag-completions))) > + (remove-tag-list (mapcar (apply-partially 'concat "-") > + (notmuch-tag-completions search-terms))) This will make two calls to notmuch search, but often one will suffice. It's probably worth optimizing the case were search-terms is nil. > + (tag-list (append add-tag-list remove-tag-list)) > + (crm-separator " ") > + ;; By default, space is bound to "complete word" function. > + ;; Re-bind it to insert a space instead. Note that > + ;; still does the completion. > + (crm-local-completion-map > + (let ((map (make-sparse-keymap))) > + (set-keymap-parent map crm-local-completion-map) > + (define-key map " " 'self-insert-command) > + map))) > + (delete "" (completing-read-multiple > + "Operations (+add -drop): notmuch tag " tag-list nil I don't think the "notmuch tag" part is necessary. From the perspective of a person who only uses the Emacs UI, this will be meaningless. Maybe "Tag changes (+add -drop): " or even just "Tags (+add -drop): " since the "+add -drop" part implies what you're doing. > + nil initial-input)))) > + > +(defun notmuch-update-tags (current-tags changed-tags) Maybe just "tags" instead of "current-tags"? Nothing says they have to be current. It's just a list of tags. Also, changed-tags makes it sound like a list of tags, which is isn't. Maybe tag-changes? > + "Update `current-tags' with `changed-tags' and return the result. > + > +`Changed-tags' is a list of tag operations given to \"notmuch > +tag\", i.e. a list of changed tags with '+' and '-' prefixes." Same comment about ticks and "notmuch tag". I found this docstring a bit confusing. I wasn't sure exactly what it meant to "update current-tags with changed-tags" (though replacing changed-tags with tag-changes would probably help). Plus, this function does not, in fact, update current-tags. Maybe, "Return a copy of TAGS with additions and removals from TAG-CHANGES. TAG-CHANGES must be a list of tags names, each prefixed with either a \"+\" to indicate the tag should be added to TAGS if not present or a \"-\" to indicate that the tag should be removed from TAGS if present." > + (let ((result-tags (copy-sequence current-tags))) > + (mapc (lambda (changed-tag) Consider dolist instead of mapc, though this is a matter of taste. It leads to less indentation (and does have precedent in the notmuch code, though mapc is more common). Too bad Elisp doesn't have fold. > + (unless (string= changed-tag "") > + (let ((op (substring changed-tag 0 1)) > + (tag (substring changed-tag 1))) > + (cond ((string= op "+") > + (unless (member tag result-tags) > + (push tag result-tags))) > + ((string= op "-") > + (setq result-tags (delete tag result-tags))) > + (t > + (error "Changed tag must be of the form `+this_tag' or `-that_tag'")))))) I would suggest case instead of cond, but, again, that's a matter of taste. > + changed-tags) > + (sort result-tags 'string<))) > > (defun notmuch-foreach-mime-part (function mm-handle) > (cond ((stringp (car mm-handle)) > @@ -447,6 +465,10 @@ Complete list of currently available key bindings: > "Return a list of threads for the current region" > (notmuch-search-properties-in-region 'notmuch-search-thread-id beg end)) > > +(defun notmuch-search-find-thread-id-region-search (beg end) > + "Return a search string for threads for the current region" > + (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or ")) > + > (defun notmuch-search-find-authors () > "Return the authors for the current thread" > (get-text-property (point) 'notmuch-search-authors)) > @@ -590,74 +612,55 @@ the messages that were tagged" > (forward-line 1)) > output))) > > -(defun notmuch-search-add-tag-thread (tag) > - (notmuch-search-add-tag-region tag (point) (point))) > +(defun notmuch-search-tag-thread (&rest tags) > + "Change tags for the currently selected thread. > > -(defun notmuch-search-add-tag-region (tag beg end) > - (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or "))) > - (notmuch-tag search-id-string (concat "+" tag)) > - (save-excursion > - (let ((last-line (line-number-at-pos end)) > - (max-line (- (line-number-at-pos (point-max)) 2))) > - (goto-char beg) > - (while (<= (line-number-at-pos) (min last-line max-line)) > - (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))) > - (forward-line)))))) > +See `notmuch-search-tag-region' for details." > + (apply 'notmuch-search-tag-region (point) (point) tags)) > > -(defun notmuch-search-remove-tag-thread (tag) > - (notmuch-search-remove-tag-region tag (point) (point))) > +(defun notmuch-search-tag-region (beg end &rest tags) > + "Change tags for threads in the given region. > > -(defun notmuch-search-remove-tag-region (tag beg end) > - (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or "))) > - (notmuch-tag search-id-string (concat "-" tag)) > +`Tags' is a list of tag operations for \"notmuch tag\", i.e. a > +list of tags to change with '+' and '-' prefixes. The tags are > +added or removed for all threads in the region from `beg' to > +`end'." Same comment about ticks and "notmuch tag". > + (let ((search-string (notmuch-search-find-thread-id-region-search beg end))) > + (apply 'notmuch-tag search-string tags) > (save-excursion > (let ((last-line (line-number-at-pos end)) > (max-line (- (line-number-at-pos (point-max)) 2))) > (goto-char beg) > (while (<= (line-number-at-pos) (min last-line max-line)) > - (notmuch-search-set-tags (delete tag (notmuch-search-get-tags))) > + (notmuch-search-set-tags > + (notmuch-update-tags (notmuch-search-get-tags) tags)) > (forward-line)))))) > > -(defun notmuch-search-add-tag (tag) > - "Add a tag to the currently selected thread or region. > - > -The tag is added to all messages in the currently selected thread > -or threads in the current region." > - (interactive > - (list (notmuch-select-tag-with-completion "Tag to add: "))) > - (save-excursion > - (if (region-active-p) > - (let* ((beg (region-beginning)) > - (end (region-end))) > - (notmuch-search-add-tag-region tag beg end)) > - (notmuch-search-add-tag-thread tag)))) > - > -(defun notmuch-search-remove-tag (tag) > - "Remove a tag from the currently selected thread or region. > +(defun notmuch-search-tag (&optional initial-input) > + "Change tags for the currently selected thread or region." > + (interactive) > + (let* ((beg (if (region-active-p) (region-beginning) (point))) > + (end (if (region-active-p) (region-end) (point))) While you're in here, these should probably be `use-region-p'. > + (search-string (notmuch-search-find-thread-id-region-search beg end)) > + (tags (notmuch-select-tags-with-completion initial-input search-string))) > + (apply 'notmuch-search-tag-region beg end tags))) > + > +(defun notmuch-search-add-tag () > + "Same as `notmuch-search-tag' but sets initial input to '+'." > + (interactive) > + (notmuch-search-tag "+")) > > -The tag is removed from all messages in the currently selected > -thread or threads in the current region." > - (interactive > - (list (notmuch-select-tag-with-completion > - "Tag to remove: " > - (if (region-active-p) > - (mapconcat 'identity > - (notmuch-search-find-thread-id-region (region-beginning) (region-end)) > - " ") > - (notmuch-search-find-thread-id))))) > - (save-excursion > - (if (region-active-p) > - (let* ((beg (region-beginning)) > - (end (region-end))) > - (notmuch-search-remove-tag-region tag beg end)) > - (notmuch-search-remove-tag-thread tag)))) > +(defun notmuch-search-remove-tag () > + "Same as `notmuch-search-tag' but sets initial input to '-'." > + (interactive) > + (notmuch-search-tag "-")) > > (defun notmuch-search-archive-thread () > "Archive the currently selected thread (remove its \"inbox\" tag). > > This function advances the next thread when finished." > (interactive) > - (notmuch-search-remove-tag-thread "inbox") > + (notmuch-search-tag-thread "-inbox") > (notmuch-search-next-thread)) > > (defvar notmuch-search-process-filter-data nil > @@ -893,9 +896,7 @@ will prompt for tags to be added or removed. Tags prefixed with > Each character of the tag name may consist of alphanumeric > characters as well as `_.+-'. > " > - (interactive (notmuch-select-tags-with-completion > - "Operations (+add -drop): notmuch tag " > - '("+" "-"))) > + (interactive (notmuch-select-tags-with-completion)) > (apply 'notmuch-tag notmuch-search-query-string actions)) > > (defun notmuch-search-buffer-title (query)