Merge branch 'improvements-to-agenda-filters'

This commit is contained in:
Carsten Dominik 2019-08-29 22:04:15 +02:00
commit 3ac2fb6c5f
3 changed files with 240 additions and 61 deletions

View File

@ -9109,12 +9109,6 @@ custom agenda commands.
option ~org-agenda-category-filter-preset~. See [[*Setting options
for custom commands]].
- {{{kbd(^)}}} (~org-agenda-filter-by-top-headline~) ::
#+findex: org-agenda-filter-by-top-headline
Filter the current agenda view and only display the siblings and the
parent headline of the one at point.
- {{{kbd(=)}}} (~org-agenda-filter-by-regexp~) ::
#+findex: org-agenda-filter-by-regexp
@ -9160,6 +9154,35 @@ custom agenda commands.
option ~org-agenda-effort-filter-preset~. See [[*Setting options for
custom commands]].
- {{{kbd(\)}}} (~org-agenda-filter~) ::
#+findex: org-agenda-filter
This is an alternative interface to all four filter methods
described above. At the prompt, one would specify different filter
elements in a single string, with full completion support. For
example,
#+begin_example
+work-John<0:10-/plot/
#+end_example
selects entries with category `work' and effort estimates below 10
minutes, and deselects entries with tag `John' or matching the
regexp `plot'. `+' can be left out if that does not lead to
ambiguities. The sequence of elements is arbitrary. The filter
syntax assumes that there is no overlap between categories and tags
(tags will take priority). If you reply to the prompt with the
empty string, all filtering is removed. If a filter is specified,
it replaces all current filters. But if you call the command with a
prefix argument, the new filter elements are added to the active
ones.
- {{{kbd(^)}}} (~org-agenda-filter-by-top-headline~) ::
#+findex: org-agenda-filter-by-top-headline
Filter the current agenda view and only display the siblings and the
parent headline of the one at point.
- {{{kbd(|)}}} (~org-agenda-filter-remove-all~) ::
Remove all filters in the current agenda view.

View File

@ -2402,6 +2402,7 @@ The following commands are available:
(org-defkey org-agenda-mode-map "/" 'org-agenda-filter-by-tag)
(org-defkey org-agenda-mode-map "_" 'org-agenda-filter-by-effort)
(org-defkey org-agenda-mode-map "=" 'org-agenda-filter-by-regexp)
(org-defkey org-agenda-mode-map "\\" 'org-agenda-filter)
(org-defkey org-agenda-mode-map "|" 'org-agenda-filter-remove-all)
(org-defkey org-agenda-mode-map "~" 'org-agenda-limit-interactively)
(org-defkey org-agenda-mode-map "<" 'org-agenda-filter-by-category)
@ -2482,8 +2483,20 @@ The following commands are available:
:keys "v A"]
"--"
["Remove Restriction" org-agenda-remove-restriction-lock org-agenda-restrict])
["Write view to file" org-agenda-write t]
("Filter current view"
["with generic interface" org-agenda-filter t]
"--"
["by category at cursor" org-agenda-filter-by-category t]
["by tag" org-agenda-filter-by-tag t]
["by effort" org-agenda-filter-by-effort t]
["by regexp" org-agenda-filter-by-regexp t]
["by top-level headline" org-agenda-filter-by-top-headline t]
"--"
["Remove all filtering" org-agenda-filter-remove-all t]
"--"
["limit" org-agenda-limit-interactively t])
["Rebuild buffer" org-agenda-redo t]
["Write view to file" org-agenda-write t]
["Save all Org buffers" org-save-all-org-buffers t]
"--"
["Show original entry" org-agenda-show t]
@ -3626,6 +3639,11 @@ removed from the entry content. Currently only `planning' is allowed here."
(defvar org-agenda-regexp-filter nil)
(defvar org-agenda-effort-filter nil)
(defvar org-agenda-top-headline-filter nil)
(defvar org-agenda-represented-categories nil
"Cache for the list of all categories in the agenda.")
(defvar org-agenda-represented-tags nil
"Cache for the list of all categories in the agenda.")
(defvar org-agenda-tag-filter-preset nil
"A preset of the tags filter used for secondary agenda filtering.
This must be a list of strings, each string must be a single tag preceded
@ -3636,6 +3654,20 @@ the entire agenda view. In a block agenda, it will not work reliably to
define a filter for one of the individual blocks. You need to set it in
the global options and expect it to be applied to the entire view.")
(defconst org-agenda-filter-variables
'((category . org-agenda-category-filter)
(tag . org-agenda-tag-filter)
(effort . org-agenda-effort-filter)
(regexp . org-agenda-regexp-filter))
"Alist of filter types and associated variables")
(defun org-agenda-filter-any ()
"Is any filter active?"
(let ((form (cons 'or (mapcar (lambda (x)
(if (or (symbol-value (cdr x))
(get :preset-filter x))
t nil))
org-agenda-filter-variables))))
(eval form)))
(defvar org-agenda-category-filter-preset nil
"A preset of the category filter used for secondary agenda filtering.
This must be a list of strings, each string must be a single category
@ -3733,6 +3765,7 @@ FILTER-ALIST is an alist of filters we need to apply when
(put 'org-agenda-tag-filter :preset-filter nil)
(put 'org-agenda-category-filter :preset-filter nil)
(put 'org-agenda-regexp-filter :preset-filter nil)
(put 'org-agenda-effort-filter :preset-filter nil)
;; Popup existing buffer
(org-agenda-prepare-window (get-buffer org-agenda-buffer-name)
filter-alist)
@ -3834,6 +3867,8 @@ FILTER-ALIST is an alist of filters we need to apply when
(org-with-point-at mrk
(mapcar #'downcase (org-get-tags)))))))))
(run-hooks 'org-agenda-finalize-hook)
(setq org-agenda-represented-tags nil
org-agenda-represented-categories nil)
(when org-agenda-top-headline-filter
(org-agenda-filter-top-headline-apply
org-agenda-top-headline-filter))
@ -7429,17 +7464,24 @@ With a prefix argument, do so in all agenda buffers."
"Return the category of the agenda line."
(org-get-at-bol 'org-category))
(defun org-agenda-filter-by-category (strip)
"Filter lines in the agenda buffer that have a specific category.
The category is that of the current line.
Without prefix argument, keep only the lines of that category.
With a prefix argument, exclude the lines of that category.
"
Without prefix argument STRIP, keep only the lines of that category.
With a prefix argument, exclude the lines of that category."
(interactive "P")
(if (and org-agenda-filtered-by-category
org-agenda-category-filter)
(org-agenda-filter-show-all-cat)
(let ((cat (org-no-properties (org-agenda-get-category))))
(progn
(org-agenda-filter-show-all-cat)
(message "All categories are shown"))
(let* ((categories (org-agenda-get-represented-categories))
(defcat (org-no-properties (or (org-agenda-get-category)
(car categories))))
(cat (completing-read (format "Category [%s]: " defcat)
(org-agenda-get-represented-categories)
nil t nil nil defcat)))
(cond
((and cat strip)
(org-agenda-filter-apply
@ -7514,30 +7556,134 @@ With two prefix arguments, remove the effort filters."
(mapcar (lambda (n) (mod n 10)) ;turn 10 into 0
(number-sequence 1 (length efforts)))))
(op nil))
(while (not (memq op '(?< ?> ?=)))
(setq op (read-char-exclusive "Effort operator? (> = or <)")))
(while (not (memq op '(?< ?> ?= ?_)))
(setq op (read-char-exclusive "Effort operator? (> = or <) or press `_' again to remove filter")))
;; Select appropriate duration. Ignore non-digit characters.
(let ((prompt
(apply #'format
(concat "Effort %c "
(mapconcat (lambda (s) (concat "[%d]" s))
efforts
" "))
op allowed-keys))
(eff -1))
(while (not (memq eff allowed-keys))
(message prompt)
(setq eff (- (read-char-exclusive) 48)))
(setq org-agenda-effort-filter
(list (concat (if strip "-" "+")
(char-to-string op)
;; Numbering is 1 2 3 ... 9 0, but we want
;; 0 1 2 ... 8 9.
(nth (mod (1- eff) 10) efforts)))))
(org-agenda-filter-apply org-agenda-effort-filter 'effort)))
(if (eq op ?_)
(progn
(org-agenda-filter-show-all-effort)
(message "Effort filter removed"))
(let ((prompt
(apply #'format
(concat "Effort %c "
(mapconcat (lambda (s) (concat "[%d]" s))
efforts
" "))
op allowed-keys))
(eff -1))
(while (not (memq eff allowed-keys))
(message prompt)
(setq eff (- (read-char-exclusive) 48)))
(setq org-agenda-effort-filter
(list (concat (if strip "-" "+")
(char-to-string op)
;; Numbering is 1 2 3 ... 9 0, but we want
;; 0 1 2 ... 8 9.
(nth (mod (1- eff) 10) efforts)))))
(org-agenda-filter-apply org-agenda-effort-filter 'effort))))
(t (org-agenda-filter-show-all-effort)
(message "Effort filter removed"))))
(defun org-agenda-filter (&optional keep)
"Prompt for a general filter string and apply it to the agenda.
The new filter replaces all existing elements. When called with a
prefix arg KEEP, add the new elements to the existing filter.
The string may contain filter elements like
+category
+tag
+<effort > and = are also allowed as effort operators
+/regexp/
Instead of `+', `-' is allowed to strip the agenda of matching entries.
`+' is optional if it is not required to separate two string parts.
Multiple filter elements can be concatenated without spaces, for example
+work-John<0:10-/plot/
selects entries with category `work' and effort estimates below 10 minutes,
and deselects entries with tag `John' or matching the regexp `plot'.
During entry of the filter, completion for tags, categories and effort
values is offered. Since the syntax for categories and tags is identical
there should be no overlap between categoroes and tags. If there is, tags
get priority."
(interactive "P")
(let* ((tag-list (org-agenda-get-represented-tags))
(category-list (org-agenda-get-represented-categories))
(f-string (completing-read "Filter [+cat-tag<0:10-/regexp/]: " 'org-agenda-filter-completion-function))
(fc (if keep org-agenda-category-filter))
(ft (if keep org-agenda-tag-filter))
(fe (if keep org-agenda-effort-filter))
(fr (if keep org-agenda-regexp-filter))
log s)
(while (string-match "^[ \t]*\\([-+]\\)?\\(\\([^-+<>=/ \t]+\\)\\|\\([<>=][0-9:]+\\)\\|\\(/\\([^/]+\\)/?\\)\\)"
f-string)
(setq log (if (match-beginning 1) (match-string 1 f-string) "+"))
(cond
((match-beginning 3)
;; category or tag
(setq s (match-string 3 f-string))
(cond ((member s tag-list)
(push (concat log s) ft))
((member s category-list)
(push (concat log s) fc))
(t (message "`%s%s' filter ignored because it is not represented as tag or category" log s))))
((match-beginning 4)
;; effort
(push (concat log (match-string 4 f-string)) fe))
((match-beginning 5)
;; regexp
(push (concat log (match-string 6 f-string)) fr)))
(setq f-string (substring f-string (match-end 0))))
(org-agenda-filter-remove-all)
(and fc (org-agenda-filter-apply
(setq org-agenda-category-filter fc) 'category))
(and ft (org-agenda-filter-apply
(setq org-agenda-tag-filter ft) 'tag))
(and fe (org-agenda-filter-apply
(setq org-agenda-effort-filter fe) 'effort))
(and fr (org-agenda-filter-apply
(setq org-agenda-regexp-filter fr) 'regexp))
))
(defun org-agenda-filter-completion-function (string _predicate &optional flag)
"Complete a complex filter string
FLAG specifies the type of completion operation to perform. This
function is passed as a collection function to `completing-read',
which see."
(let ((completion-ignore-case t) ;tags are case-sensitive
(confirm (lambda (x) (stringp x)))
(prefix "")
(operator "")
table)
(when (string-match "^\\(.*\\([-+<>=]\\)\\)\\([^-+<>=]*\\)$" string)
(setq prefix (match-string 1 string)
operator (match-string 2 string)
string (match-string 3 string)))
(cond
((member operator '("+" "-" "" nil))
(setq table (append (org-agenda-get-represented-categories)
(org-agenda-get-represented-tags))))
((member operator '("<" ">" "="))
(setq table (split-string
(or (cdr (assoc (concat org-effort-property "_ALL")
org-global-properties))
"0 0:10 0:30 1:00 2:00 3:00 4:00 5:00 6:00 7:00")
" +")))
(t (setq table nil)))
(pcase flag
(`t (all-completions string table confirm))
(`lambda (assoc string table)) ;exact match?
(`nil
(pcase (try-completion string table confirm)
((and completion (pred stringp))
(concat prefix completion))
(completion completion)))
(_ nil))))
(defun org-agenda-filter-remove-all ()
"Remove all filters from the current agenda buffer."
(interactive)
@ -7637,17 +7783,32 @@ also press `-' or `+' to switch between filtering and excluding."
(org-agenda-filter-apply org-agenda-tag-filter 'tag expand))
(t (error "Invalid tag selection character %c" char)))))
(defun org-agenda-get-represented-tags ()
"Get a list of all tags currently represented in the agenda."
(let (p tags)
(save-excursion
(goto-char (point-min))
(while (setq p (next-single-property-change (point) 'tags))
(goto-char p)
(mapc (lambda (x) (add-to-list 'tags x))
(get-text-property (point) 'tags))))
tags))
(defun org-agenda-get-represented-categories ()
"Return a list of all categories used in this agenda buffer."
(or org-agenda-represented-categories
(when (derived-mode-p 'org-agenda-mode)
(let ((pos (point-min)) categories)
(while (and (< pos (point-max))
(setq pos (next-single-property-change
pos 'org-category nil (point-max))))
(push (get-text-property pos 'org-category) categories))
(setq org-agenda-represented-categories
(nreverse (org-uniquify (delq nil categories))))))))
(defun org-agenda-get-represented-tags ()
"Return a list of all tags used in this agenda buffer.
These will be lower-case, for filtering."
(or org-agenda-represented-tags
(when (derived-mode-p 'org-agenda-mode)
(let ((pos (point-min)) tags-lists tt)
(while (and (< pos (point-max))
(setq pos (next-single-property-change
pos 'tags nil (point-max))))
(setq tt (get-text-property pos 'tags))
(if tt (push tt tags-lists)))
(setq org-agenda-represented-tags
(nreverse (org-uniquify
(delq nil (apply 'append tags-lists)))))))))
(defun org-agenda-filter-make-matcher (filter type &optional expand)
"Create the form that tests a line for agenda filter. Optional
@ -8350,56 +8511,51 @@ When called with a prefix argument, include all archive files as well."
((eq org-agenda-show-log 'clockcheck) " ClkCk")
(org-agenda-show-log " Log")
(t ""))
(if (org-agenda-filter-any) " " "")
(if (or org-agenda-category-filter
(get 'org-agenda-category-filter :preset-filter))
'(:eval (propertize
(concat " <"
(concat "["
(mapconcat
'identity
(append
(get 'org-agenda-category-filter :preset-filter)
org-agenda-category-filter)
"")
">")
"]")
'face 'org-agenda-filter-category
'help-echo "Category used in filtering")) "")
(if (or org-agenda-tag-filter
(get 'org-agenda-tag-filter :preset-filter))
'(:eval (propertize
(concat " {"
(mapconcat
(concat (mapconcat
'identity
(append
(get 'org-agenda-tag-filter :preset-filter)
org-agenda-tag-filter)
"")
"}")
""))
'face 'org-agenda-filter-tags
'help-echo "Tags used in filtering")) "")
(if (or org-agenda-effort-filter
(get 'org-agenda-effort-filter :preset-filter))
'(:eval (propertize
(concat " {"
(mapconcat
(concat (mapconcat
'identity
(append
(get 'org-agenda-effort-filter :preset-filter)
org-agenda-effort-filter)
"")
"}")
""))
'face 'org-agenda-filter-effort
'help-echo "Effort conditions used in filtering")) "")
(if (or org-agenda-regexp-filter
(get 'org-agenda-regexp-filter :preset-filter))
'(:eval (propertize
(concat " ["
(mapconcat
'identity
(concat (mapconcat
(lambda (x) (concat (substring x 0 1) "/" (substring x 1) "/"))
(append
(get 'org-agenda-regexp-filter :preset-filter)
org-agenda-regexp-filter)
"")
"]")
""))
'face 'org-agenda-filter-regexp
'help-echo "Regexp used in filtering")) "")
(if org-agenda-archives-mode

View File

@ -559,10 +559,6 @@ month and 365.24 days for a year)."
"Face for tag(s) in the mode-line when filtering the agenda."
:group 'org-faces)
(defface org-agenda-filter-regexp '((t :inherit mode-line))
"Face for regexp(s) in the mode-line when filtering the agenda."
:group 'org-faces)
(defface org-agenda-filter-category '((t :inherit mode-line))
"Face for categories in the mode-line when filtering the agenda."
:group 'org-faces)
@ -571,6 +567,10 @@ month and 365.24 days for a year)."
"Face for effort in the mode-line when filtering the agenda."
:group 'org-faces)
(defface org-agenda-filter-regexp '((t :inherit mode-line))
"Face for regexp(s) in the mode-line when filtering the agenda."
:group 'org-faces)
(defface org-time-grid ;Copied from `font-lock-variable-name-face'
'((((class color) (min-colors 16) (background light)) (:foreground "DarkGoldenrod"))
(((class color) (min-colors 16) (background dark)) (:foreground "LightGoldenrod"))