branch: externals/org commit b02c34948b051f8714c682f22f9a9cfc91c0501e Author: Morgan Smith <morgan.j.sm...@outlook.com> Commit: Ihor Radchenko <yanta...@posteo.net>
Allow `org-tags-sort-function' to be a list of functions * lisp/org.el (org-tags-sort-function): Describe new feature. Add '(repeat function) to the type. (org-tags-sort): New function * lisp/org-agenda.el (org-cmp-tag): Use `org-tags-sort'. * lisp/org-mouse.el (org-mouse-tag-menu, org-mouse-popup-global-menu): Use `org-tags-sort'. * testing/lisp/test-org-agenda.el (test-org-agenda/tags-sorting): Test new functionality. * etc/ORG-NEWS: Announce the new feature. --- etc/ORG-NEWS | 6 ++++++ lisp/org-agenda.el | 4 ++-- lisp/org-mouse.el | 8 ++++---- lisp/org.el | 38 ++++++++++++++++++++++++++++++++++---- testing/lisp/test-org-agenda.el | 32 ++++++++++++++++++++++++-------- 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 375a0c4241..a7763ef517 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -358,6 +358,12 @@ behaviour of other exporters. In this case, to exclude a section from the table of contents, mark it as =:UNNUMBERED: notoc= in its properties. +*** ~org-tags-sort-function~ can now be a list of functions + +~org-tags-sort-function~ can now be set to a list of functions. +Subsequent sorting functions will be used if two tags are found to be +equivalent. See docstring for more information. + *** New option ~org-cite-basic-complete-key-crm-separator~ This option makes ~org-cite~'s ~basic~ insert processor use diff --git a/lisp/org-agenda.el b/lisp/org-agenda.el index 7f0a6ee759..a10ae18884 100644 --- a/lisp/org-agenda.el +++ b/lisp/org-agenda.el @@ -7570,8 +7570,8 @@ The optional argument TYPE tells the agenda type." (cond ((not (or ta tb)) nil) ((not ta) +1) ((not tb) -1) - ((funcall (or org-tags-sort-function #'org-string<) ta tb) -1) - ((funcall (or org-tags-sort-function #'org-string<) tb ta) +1)))) + ((org-tags-sort ta tb) -1) + ((org-tags-sort tb ta) +1)))) (defsubst org-cmp-time (a b) "Compare the time-of-day values of strings A and B." diff --git a/lisp/org-mouse.el b/lisp/org-mouse.el index a282f004c7..bc0857d3c3 100644 --- a/lisp/org-mouse.el +++ b/lisp/org-mouse.el @@ -427,13 +427,13 @@ SCHEDULED: or DEADLINE: or ANYTHINGLIKETHIS:" (let ((tags (org-get-tags nil t))) (org-mouse-keyword-menu (sort (mapcar #'car (org-get-buffer-tags)) - (or org-tags-sort-function #'org-string<)) + #'org-tags-sort) (lambda (tag) (org-mouse-set-tags (sort (if (member tag tags) (delete tag tags) (cons tag tags)) - (or org-tags-sort-function #'org-string<)))) + #'org-tags-sort))) (lambda (tag) (member tag tags)) )) '("--" @@ -504,7 +504,7 @@ SCHEDULED: or DEADLINE: or ANYTHINGLIKETHIS:" ("Check Tags" ,@(org-mouse-keyword-menu (sort (mapcar #'car (org-get-buffer-tags)) - (or org-tags-sort-function #'org-string<)) + #'org-tags-sort) (lambda (tag) (org-tags-sparse-tree nil tag))) "--" ["Custom Tag ..." org-tags-sparse-tree t]) @@ -515,7 +515,7 @@ SCHEDULED: or DEADLINE: or ANYTHINGLIKETHIS:" ("Display Tags" ,@(org-mouse-keyword-menu (sort (mapcar #'car (org-get-buffer-tags)) - (or org-tags-sort-function #'org-string<)) + #'org-tags-sort) (lambda (tag) (org-tags-view nil tag))) "--" ["Custom Tag ..." org-tags-view t]) diff --git a/lisp/org.el b/lisp/org.el index a98adf927c..178611f8e8 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -3009,13 +3009,24 @@ is better to limit inheritance to certain tags using the variables "When set, tags are sorted using this function as a comparator. When the value is nil, use default sorting order. The default sorting is alphabetical, except in `org-set-tags' where no sorting is done by -default." +default. + +This can also be a list of functions. To enable advanced sorting +algorithms a special algorithm is used. If a sorting function returns +nil when comparing two tags, then it is tried again with the tags in the +opposite order. If the function once again returns nil, it is assumed +that both tags are deemed equal and they will then be sorted by the next +sort function in the list. + +A sort function can call `org-tag-sort' which will use the next sort +function in the list." :group 'org-tags :type '(choice (const :tag "Default sorting" nil) (const :tag "Alphabetical" org-string<) (const :tag "Reverse alphabetical" org-string>) - (function :tag "Custom function" nil))) + (function :tag "Custom function" nil) + (repeat function))) (defvar org-tags-history nil "History of minibuffer reads for tags.") @@ -4343,6 +4354,25 @@ See `org-tag-alist' for their structure." ;; Preserve order of ALIST1. (append (nreverse to-add) alist2))))) +(defun org-tags-sort (tag1 tag2) + "Sort tags TAG1 and TAG2 according to the value of `org-tags-sort-function'." + (let ((org-tags-sort-function + (cond ((functionp org-tags-sort-function) (list org-tags-sort-function)) + ((consp org-tags-sort-function) org-tags-sort-function) + ;; Default sorting as described in docstring of `org-tags-sort-function'. + ((null org-tags-sort-function) (list #'org-string<))))) + (catch :org-tags-sort-return + (dolist (sort-fun org-tags-sort-function) + ;; So the function can call `org-tags-sort' + (let ((org-tags-sort-function (cdr org-tags-sort-function))) + (cond + ((funcall sort-fun tag1 tag2) ; tag1 < tag2 + (throw :org-tags-sort-return t)) + ((funcall sort-fun tag2 tag1) ; tag1 > tag2 + (throw :org-tags-sort-return nil)) + (t ; tag1 = tag2 + 'continue-loop))))))) + (defun org-priority-to-value (s) "Convert priority string S to its numeric value." (or (save-match-data @@ -12123,8 +12153,8 @@ This function assumes point is on a headline." (_ (error "Invalid tag specification: %S" tags)))) (old-tags (org-get-tags nil t)) (tags-change? nil)) - (when (functionp org-tags-sort-function) - (setq tags (sort tags org-tags-sort-function))) + (when org-tags-sort-function + (setq tags (sort tags #'org-tags-sort))) (setq tags-change? (not (equal tags old-tags))) (when tags-change? ;; Delete previous tags and any trailing white space. diff --git a/testing/lisp/test-org-agenda.el b/testing/lisp/test-org-agenda.el index 06d5abc43a..3c2102905e 100644 --- a/testing/lisp/test-org-agenda.el +++ b/testing/lisp/test-org-agenda.el @@ -657,7 +657,9 @@ Sunday 7 January 2024 (ert-deftest test-org-agenda/tags-sorting () "Test if `org-agenda' sorts tags according to `org-tags-sort-function'." - (let ((org-agenda-custom-commands + (let ((string-length< (lambda (s1 s2) + (< (length s1) (length s2)))) + (org-agenda-custom-commands '(("f" "no fluff" todo "" ((org-agenda-todo-keyword-format "") (org-agenda-overriding-header "") @@ -667,14 +669,19 @@ Sunday 7 January 2024 (org-test-agenda-with-agenda (string-join '("* TODO group_a :group_a:" - "* TODO tag_a_1 :tag_a_1:" "* TODO tag_a_2 :tag_a_2:" - "* TODO tag_b_1 :tag_b_1:" "* TODO tag_b_2 :tag_b_2:" "* TODO groupless :groupless:" - "* TODO lonely :lonely:") + "* TODO tag_a_1 :tag_a_1:" + "* TODO tag_b_1 :tag_b_1:" + "* TODO lonely :lonely:" + "* TODO blueberry :blueberry:") "\n") - (dolist (org-tags-sort-function '(nil org-string< org-string> ignore)) + (dolist (org-tags-sort-function `(nil + org-string< org-string> ignore + ,string-length< + (,string-length<) + (,string-length< org-string<))) (should (string-equal (string-trim @@ -685,15 +692,24 @@ Sunday 7 January 2024 ;; Not sorted ('ignore (string-join - '("group_a" "tag_a_1" "tag_a_2" "tag_b_1" "tag_b_2" "groupless" "lonely") + '("group_a" "tag_a_2" "tag_b_2" "groupless" "tag_a_1" "tag_b_1" "lonely" "blueberry") "\n")) ((or 'nil 'org-string<) (string-join - '("group_a" "groupless" "lonely" "tag_a_1" "tag_a_2" "tag_b_1" "tag_b_2") + '("blueberry" "group_a" "groupless" "lonely" "tag_a_1" "tag_a_2" "tag_b_1" "tag_b_2") "\n")) ('org-string> (string-join - '("tag_b_2" "tag_b_1" "tag_a_2" "tag_a_1" "lonely" "groupless" "group_a") + '("tag_b_2" "tag_b_1" "tag_a_2" "tag_a_1" "lonely" "groupless" "group_a" "blueberry") + "\n")) + ((or (pred (equal string-length<)) + `(,string-length<)) + (string-join + '("lonely" "group_a" "tag_a_2" "tag_b_2" "tag_a_1" "tag_b_1" "groupless" "blueberry") + "\n")) + (`(,string-length< org-string<) + (string-join + '("lonely" "group_a" "tag_a_1" "tag_a_2" "tag_b_1" "tag_b_2" "blueberry" "groupless") "\n"))))))))) (ert-deftest test-org-agenda/goto-date ()