Return-Path: X-Original-To: notmuch@notmuchmail.org Delivered-To: notmuch@notmuchmail.org Received: from localhost (localhost [127.0.0.1]) by olra.theworths.org (Postfix) with ESMTP id 18AE1431FD2 for ; Sun, 29 Jan 2012 17:33:57 -0800 (PST) X-Virus-Scanned: Debian amavisd-new at olra.theworths.org X-Spam-Flag: NO X-Spam-Score: -0.799 X-Spam-Level: X-Spam-Status: No, score=-0.799 tagged_above=-999 required=5 tests=[DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, FREEMAIL_FROM=0.001, 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 xJ0H9W9noKAI for ; Sun, 29 Jan 2012 17:33:55 -0800 (PST) Received: from mail-bk0-f53.google.com (mail-bk0-f53.google.com [209.85.214.53]) (using TLSv1 with cipher RC4-SHA (128/128 bits)) (No client certificate requested) by olra.theworths.org (Postfix) with ESMTPS id 349D9431FBC for ; Sun, 29 Jan 2012 17:33:55 -0800 (PST) Received: by bke11 with SMTP id 11so933769bke.26 for ; Sun, 29 Jan 2012 17:33:53 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=from:to:cc:subject:in-reply-to:references:user-agent:date :message-id:mime-version:content-type; bh=VUjLvaWi8mWuLvqUj1xpTKot7UV3F0gzEz05pHA6MPk=; b=pByzmrefDaF1Ti0hZZ2KIh5MBpM30RFsF471GJcg50ORLplS2EYOGEjIurj9DZIv4w /0i4T8r3Z+3ioMQyyQpkWBdJ/tJsThtJfCQTeyUOO+AlH8wvo3v30XkLFGC3YRSPTBlh lyTRx1tugdYh8Viu2/AIYY9wJ0vSQAphN/LFU= Received: by 10.204.153.27 with SMTP id i27mr7672093bkw.81.1327887233783; Sun, 29 Jan 2012 17:33:53 -0800 (PST) Received: from localhost ([91.144.186.21]) by mx.google.com with ESMTPS id ci12sm33968605bkb.13.2012.01.29.17.33.52 (version=TLSv1/SSLv3 cipher=OTHER); Sun, 29 Jan 2012 17:33:53 -0800 (PST) From: Dmitry Kurochkin To: Austin Clements Subject: Re: [PATCH 3/6] emacs: make "+" and "-" tagging operations more robust In-Reply-To: <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> <20120129225710.GG17991@mit.edu> User-Agent: Notmuch/0.11+134~g7ddba9d (http://notmuchmail.org) Emacs/23.3.1 (x86_64-pc-linux-gnu) Date: Mon, 30 Jan 2012 05:32:43 +0400 Message-ID: <8762fu7z5w.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii 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: Mon, 30 Jan 2012 01:33:57 -0000 Hi Austin. The below changes will be send as v2 soon. On Sun, 29 Jan 2012 17:57:10 -0500, Austin Clements wrote: > 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. > done > > --- > > 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. > Thanks for the explanation. Fixed. > 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). > Makes sense, done. > > (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. > I am definitely ok with breaking backward-compatibility here. NEWS item is a good idea, added. > > > > (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? > How about `notmuch-read-tag-changes'? > > + (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. > done > > + (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. > Just "tags" looks good to me, changed. > > + 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. > changed > Also, changed-tags makes it sound like a list of tags, which is isn't. > Maybe tag-changes? > Replaced changed-tags with tag-changes everywhere. > > + "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." > Looks good, using your docstring now. > > + (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). > Dolist is definately better here, thanks for the suggestion. > Too bad Elisp doesn't have fold. > indeed > > + (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. > Again, `case' is definately better here, changed. > > + 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". > fixed > > + (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'. > Looks like you are right. But I think this should be a separate patch. I will provide a patch for this after this series is pushed. Regards, Dmitry > > + (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)