Fix tag groups expansion as a regexp

* lisp/org.el (org--tags-expand-group): New function.
(org-tags-expand): Refactor code.  Fix expansion of identical tag
groups in the same match string.  Fix docstring.  Remove unused
argument.
* testing/lisp/test-org.el (test-org/tags-expand): New test.

Reported-by: Omari Norman <omari@smileystation.com>
<http://lists.gnu.org/r/emacs-orgmode/2018-10/msg00360.html>
This commit is contained in:
Nicolas Goaziou 2018-11-08 18:20:57 +01:00
parent bfb946c7da
commit 9df82be074
2 changed files with 116 additions and 112 deletions

View File

@ -14083,7 +14083,20 @@ See also `org-scan-tags'."
(setq matcher `(and (member todo org-not-done-keywords) ,matcher))) (setq matcher `(and (member todo org-not-done-keywords) ,matcher)))
(cons match0 `(lambda (todo tags-list level) ,matcher))))) (cons match0 `(lambda (todo tags-list level) ,matcher)))))
(defun org-tags-expand (match &optional single-as-list downcased tags-already-expanded) (defun org--tags-expand-group (group tag-groups expanded)
"Recursively Expand all tags in GROUP, according to TAG-GROUPS.
TAG-GROUPS is the list of groups used for expansion. EXPANDED is
an accumulator used in recursive calls."
(dolist (tag group)
(unless (member tag expanded)
(let ((group (assoc tag tag-groups)))
(push tag expanded)
(when group
(setq expanded
(org--tags-expand-group (cdr group) tag-groups expanded))))))
expanded)
(defun org-tags-expand (match &optional single-as-list downcased)
"Expand group tags in MATCH. "Expand group tags in MATCH.
This replaces every group tag in MATCH with a regexp tag search. This replaces every group tag in MATCH with a regexp tag search.
@ -14100,7 +14113,7 @@ E.g., this expansion
Work|Home => {\\(?:Work\\|Lab\\|Conf\\}|Home Work|Home => {\\(?:Work\\|Lab\\|Conf\\}|Home
will match anything tagged with \"Lab\" and \"Home\", or tagged will match anything tagged with \"Lab\" and \"Home\", or tagged
with \"Conf\" and \"Home\" or tagged with \"Work\" and \"home\". with \"Conf\" and \"Home\" or tagged with \"Work\" and \"Home\".
A group tag in MATCH can contain regular expressions of its own. A group tag in MATCH can contain regular expressions of its own.
For example, a group tag \"Proj\" defined as { Proj : {P@.+} } For example, a group tag \"Proj\" defined as { Proj : {P@.+} }
@ -14112,118 +14125,61 @@ When the optional argument SINGLE-AS-LIST is non-nil, MATCH is
assumed to be a single group tag, and the function will return assumed to be a single group tag, and the function will return
the list of tags in this group. the list of tags in this group.
When DOWNCASE is non-nil, expand downcased TAGS." When DOWNCASED is non-nil, expand downcased TAGS."
(if org-group-tags (unless (org-string-nw-p match) (error "Invalid match tag: %S" match))
(let ((tag-groups
(let ((g (or org-tag-groups-alist-for-agenda org-tag-groups-alist)))
(if (not downcased) g
(mapcar (lambda (s) (mapcar #'downcase s)))))))
(cond
(single-as-list (org--tags-expand-group (list match) tag-groups nil))
(org-group-tags
(let* ((case-fold-search t) (let* ((case-fold-search t)
(stable org-mode-syntax-table) (tag-syntax org-mode-syntax-table)
(taggroups (or org-tag-groups-alist-for-agenda org-tag-groups-alist)) (group-keys (mapcar #'car tag-groups))
(taggroups (if downcased (key-regexp (concat "\\([+-]?\\)" (regexp-opt group-keys 'words)))
(mapcar (lambda (tg) (mapcar #'downcase tg)) (return-match (if downcased (downcase match) match)))
taggroups) ;; Mark regexp-expressions in the match-expression so that we
taggroups)) ;; do not replace them later on.
(taggroups-keys (mapcar #'car taggroups)) (let ((s 0))
(return-match (if downcased (downcase match) match)) (while (string-match "{.+?}" return-match s)
(count 0) (setq s (match-end 0))
(work-already-expanded tags-already-expanded) (add-text-properties
regexps-in-match tags-in-group regexp-in-group regexp-in-group-escaped) (match-beginning 0) (match-end 0) '(regexp t) return-match)))
;; @ and _ are allowed as word-components in tags. ;; @ and _ are allowed as word-components in tags.
(modify-syntax-entry ?@ "w" stable) (modify-syntax-entry ?@ "w" tag-syntax)
(modify-syntax-entry ?_ "w" stable) (modify-syntax-entry ?_ "w" tag-syntax)
;; Temporarily replace regexp-expressions in the match-expression. ;; For each tag token found in MATCH, compute a regexp and it
(while (string-match "{.+?}" return-match) (with-syntax-table tag-syntax
(cl-incf count) (replace-regexp-in-string
(push (match-string 0 return-match) regexps-in-match) key-regexp
(setq return-match (replace-match (format "<%d>" count) t nil return-match))) (lambda (m)
(while (and taggroups-keys (if (get-text-property (match-beginning 2) 'regexp m)
(with-syntax-table stable m ;regexp tag: ignore
(string-match (let* ((operator (match-string 1 m))
(concat "\\(?1:[+-]?\\)\\(?2:\\<" (tag-token (let ((tag (match-string 2 m)))
(regexp-opt taggroups-keys) "\\>\\)") (list (if downcased (downcase tag) tag))))
return-match))) regexp-tags regular-tags)
(let* ((dir (match-string 1 return-match)) ;; Partition tags between regexp and regular tags.
(tag (match-string 2 return-match)) ;; Remove curly bracket syntax from regexp tags.
(tag (if downcased (downcase tag) tag))) (dolist (tag (org--tags-expand-group tag-token tag-groups nil))
(unless (or (get-text-property 0 'grouptag (match-string 2 return-match)) (save-match-data
(member tag tags-already-expanded)) (if (string-match "{\\(.+?\\)}" tag)
(setq tags-in-group (assoc tag taggroups)) (push (match-string 1 tag) regexp-tags)
(push tag work-already-expanded) (push tag regular-tags))))
;; Recursively expand each tag in the group, if the tag hasn't ;; Replace tag token by the appropriate regexp.
;; already been expanded. Restore the match-data after all recursive calls. ;; Regular tags need to be regexp-quoted, whereas
(save-match-data ;; regexp-tags are inserted as-is.
(let (tags-expanded) (let ((regular (regexp-opt regular-tags))
(dolist (x (cdr tags-in-group)) (regexp (mapconcat #'identity regexp-tags "\\|")))
(if (and (member x taggroups-keys) (concat operator
(not (member x work-already-expanded))) (cond
(setq tags-expanded ((null regular-tags) (format "{%s}" regexp))
(delete-dups ((null regexp-tags) (format "{\\<%s\\>}" regular))
(append (t (format "{\\<%s\\>\\|%s}" regular regexp))))))))
(org-tags-expand x t downcased return-match
work-already-expanded) t t))))
tags-expanded))) (t match))))
(setq tags-expanded
(append (list x) tags-expanded)))
(setq work-already-expanded
(delete-dups
(append tags-expanded
work-already-expanded))))
(setq tags-in-group
(delete-dups (cons (car tags-in-group)
tags-expanded)))))
;; Filter tag-regexps from tags.
(setq regexp-in-group-escaped
(delq nil (mapcar (lambda (x)
(if (stringp x)
(and (equal "{" (substring x 0 1))
(equal "}" (substring x -1))
x)
x))
tags-in-group))
regexp-in-group
(mapcar (lambda (x)
(substring x 1 -1))
regexp-in-group-escaped)
tags-in-group
(delq nil (mapcar (lambda (x)
(if (stringp x)
(and (not (equal "{" (substring x 0 1)))
(not (equal "}" (substring x -1)))
x)
x))
tags-in-group)))
;; If single-as-list, do no more in the while-loop.
(if (not single-as-list)
(progn
(when regexp-in-group
(setq regexp-in-group
(concat "\\|"
(mapconcat 'identity regexp-in-group
"\\|"))))
(setq tags-in-group
(concat dir
"{\\<"
(regexp-opt tags-in-group)
"\\>"
regexp-in-group
"}"))
(when (stringp tags-in-group)
(org-add-props tags-in-group '(grouptag t)))
(setq return-match
(replace-match tags-in-group t t return-match)))
(setq tags-in-group
(append regexp-in-group-escaped tags-in-group))))
(setq taggroups-keys (delete tag taggroups-keys))))
;; Add the regular expressions back into the match-expression again.
(while regexps-in-match
(setq return-match (replace-regexp-in-string (format "<%d>" count)
(pop regexps-in-match)
return-match t t))
(cl-decf count))
(if single-as-list
(if tags-in-group tags-in-group (list return-match))
return-match))
(if single-as-list
(list (if downcased (downcase match) match))
match)))
(defun org-op-to-function (op &optional stringp) (defun org-op-to-function (op &optional stringp)
"Turn an operator into the appropriate function." "Turn an operator into the appropriate function."

View File

@ -6468,6 +6468,54 @@ Paragraph<point>"
(org-toggle-tag "foo")) (org-toggle-tag "foo"))
(buffer-string))))) (buffer-string)))))
(ert-deftest test-org/tags-expand ()
"Test `org-tags-expand' specifications."
;; Expand tag groups as a regexp enclosed withing curly brackets.
(should
(equal "{\\<[ABC]\\>}"
(org-test-with-temp-text "#+TAGS: [ A : B C ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "A")))))
(should
(equal "{\\<\\(?:Aa\\|Bb\\|Cc\\)\\>}"
(org-test-with-temp-text "#+TAGS: [ Aa : Bb Cc ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "Aa")))))
;; Preserve operator before the regexp.
(should
(equal "+{\\<[ABC]\\>}"
(org-test-with-temp-text "#+TAGS: [ A : B C ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "+A")))))
(should
(equal "-{\\<[ABC]\\>}"
(org-test-with-temp-text "#+TAGS: [ A : B C ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "-A")))))
;; Handle "|" syntax.
(should
(equal "{\\<[ABC]\\>}|D"
(org-test-with-temp-text "#+TAGS: [ A : B C ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "A|D")))))
;; Handle nested groups.
(should
(equal "{\\<[A-D]\\>}"
(org-test-with-temp-text "#+TAGS: [ A : B C ]\n#+TAGS: [ B : D ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "A")))))
;; Expand multiple occurrences of the same group.
(should
(equal "{\\<[ABC]\\>}|{\\<[ABC]\\>}"
(org-test-with-temp-text "#+TAGS: [ A : B C ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "A|A")))))
;; Preserve regexp matches.
(should
(equal "{A+}"
(org-test-with-temp-text "#+TAGS: [ A : B C ]"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "{A+}"))))))
;;; TODO keywords ;;; TODO keywords