The problem seems to be a bug in the function org-agenda-skip-if
. Here's the source code, with irrelevant portions snipped out by [...]
:
(defun org-agenda-skip-if (subtree conditions)
(let (beg end m)
(org-back-to-heading t)
(setq beg (point)
end [...])
(goto-char beg)
(and
(or
(and (memq 'scheduled conditions)
(re-search-forward org-scheduled-time-regexp end t))
(and (memq 'notscheduled conditions)
(not (re-search-forward org-scheduled-time-regexp end t)))
[...]
(and (or [...]
(setq m (memq 'todo conditions)))
(org-agenda-skip-if-todo m end)))
end)))
The idea seems to be that each clause under the outer or
calls re-search-forward
to skip things matching a regex appropriate to that clause. If the search doesn't find anything, then re-search-forward
returns nil, and the function checks the next clause. (The subroutine org-agenda-skip-if-todo
does the same thing inside that last clause.) Presumably the function as a whole is called inside a save-excursion
so that things that shouldn't be skipped aren't; the value returned on a skip (end
) is pre-computed, so the function doesn't pay attention to where it's leaving point.
The problem is that not
form wrapping re-search-forward
under the notscheduled
clause. That just means that the function keeps going, but re-search-forward
has already moved point past anything that might be found by the later re-search-forward
inside org-agenda-skip-if-todo
.
The quick and (very) dirty solution, therefore, is to redefine org-agenda-skip-if
with a version wrapping that re-search-forward
form inside its own save-excursion
.
EDIT: Here's a less dirty solution using advice and noflet:
(defun patch/org-agenda-skip-if (oldfun subtree conditions)
"Advice patching the \"bad negation\" bug in `org-agenda-skip-if'.
The function `org-agenda-skip-if' does not behave correctly when
given multiple conditions, at least one of which is a
negative (e.g., `notscheduled' or `nottodo'). This function
corrects the problem when applied as :around advice."
(let ((use-excursion t))
(noflet ((re-search-forward (&rest args)
(if use-excursion
(save-excursion (apply this-fn args))
(apply this-fn args)))
(outline-next-heading (&rest args)
(let ((use-excursion nil)) (apply this-fn args))))
(funcall oldfun subtree conditions))))
(advice-add 'org-agenda-skip-if :around #'patch/org-agenda-skip-if)
Alternatively, for your particular case, you might consider just labeling both DONE
and CANCELLED
as "done" keywords. If you do that, then they'll be skipped by default, and you can drop that second clause from the skip function:
(setq org-agenda-custom-commands
'(("+" "Scheduled"
((todo
""
((org-agenda-overriding-header "\nScheduled")
(org-agenda-skip-function
'(org-agenda-skip-entry-if 'notscheduled))))))))