org-edna-1.0.2/ 0000755 0001752 0001753 00000000000 13425536424 011647 5 ustar elpa elpa org-edna-1.0.2/Project.ede 0000644 0001752 0001753 00000001744 13237020050 013722 0 ustar elpa elpa ;; Object ede-proj-project
;; EDE Project Files are auto generated: Do Not Edit
(ede-proj-project "ede-proj-project"
:file "Project.ede"
:name "Org Edna"
:targets
(list
(ede-proj-target-elisp "ede-proj-target-elisp"
:name "compile"
:path ""
:source '("org-edna.el")
:aux-packages '("org"))
(ede-proj-target-makefile-miscelaneous "ede-proj-target-makefile-miscelaneous"
:name #("check" 0 1 (idx 2))
:path ""
:source '("org-edna-tests.el" "org-edna-tests.org" "test.mk")
:partofall nil
:submakefile "test.mk")
(ede-proj-target-aux "ede-proj-target-aux"
:name "extra"
:path ""
:source '("org-edna.org" "COPYING" "dir" "org-edna.info" ".elpaignore"))
(ede-proj-target-elisp-autoloads "ede-proj-target-elisp-autoloads"
:name "autoloads"
:path ""
:source '("org-edna.el")
:autoload-file "org-edna-autoloads.el"))
:web-site-url "https://savannah.nongnu.org/projects/org-edna-el/")
org-edna-1.0.2/.elpaignore 0000644 0001752 0001753 00000000101 13235701354 013760 0 ustar elpa elpa Project.ede
Makefile
test.mk
org-edna-tests.el
org-edna-tests.org org-edna-1.0.2/.bzrignore 0000644 0001752 0001753 00000000117 13237020050 013630 0 ustar elpa elpa *.elc
local.mk
org-edna-autoloads.el
org-edna.texi
org-edna.html
.deps
Makefile org-edna-1.0.2/org-edna.info 0000644 0001752 0001753 00000174526 13425536050 014232 0 ustar elpa elpa This is org-edna.info, produced by makeinfo version 6.5 from
org-edna.texi.
INFO-DIR-SECTION Emacs
START-INFO-DIR-ENTRY
* Org Edna: (org-edna). Extensible Dependencies ’N’ Actions for Org Mode tasks.
END-INFO-DIR-ENTRY
File: org-edna.info, Node: Top, Next: Copying, Up: (dir)
Org Edna
********
* Menu:
* Copying::
* Introduction:: A Brief Introduction to Edna
* Basic Features:: Finders and Actions
* Advanced Features::
* Extending Edna:: What else can it do?
* Contributing:: I wanna help!
* Changelog:: List of changes by version
— The Detailed Node Listing —
Introduction
* Installation and Setup:: How to install Edna
* Basic Operation:: How to use Edna
* Blockers:: Blocking a TODO Item
* Triggers:: Triggering actions after completing a task
* Syntax:: Basic explanation of Edna’s syntax
Basic Features
* Finders:: How to find targets
* Actions:: Next steps
* Getting Help:: Getting some help
Finders
* ancestors:: Find a list of ancestors
* children:: Find all immediate children
* descendants:: Find all descendants
* file:: Find a file by name
* first-child:: Find the first child of a heading
* ids:: Find a list of headings with given IDs
* match:: Good old tag matching
* next-sibling:: Find the next sibling
* next-sibling-wrap:: Find the next sibling, wrapping around
* olp:: Find a heading by its outline path
* org-file:: Find a file in org-directory
* parent:: Find a parent
* previous-sibling:: Find the previous sibling
* previous-sibling-wrap:: Find the previous sibling, with wrapping
* relatives:: Generic relative finder
* rest-of-siblings:: Find the remaining siblings
* rest-of-siblings-wrap:: Find the remaining siblings, with wrapping
* self::
* siblings:: Find all the siblings
* siblings-wrap:: Find the siblings, but wrap around
Actions
* Scheduled/Deadline:: Operate on planning information
* TODO State:: Set todo state
* Archive:: Archive targets
* Chain Property:: Copy properties from source to targets
* Clocking:: Clock in or out of a target
* Property:: Set properties of targets
* Priority:: Set priorities of targets
* Tag:: Tags of a target
* Effort:: So much effort!
Advanced Features
* Finder Cache:: Making the finders work faster
* Conditions:: More than just DONE headings
* Consideration:: Only some of them
* Conditional Forms:: If/Then/Else
* Setting the Properties:: The easy way to set BLOCKER and TRIGGER
Conditions
* Heading is DONE::
* File Has Headings::
* Heading TODO State::
* Lisp Variable Set::
* Heading Has Property::
* Regexp Search:: Search for a regular expression
* Checking Tags:: Matching against a set of tags
* Matching Headings:: Matching against a match string
* Negating Conditions:: Doing the opposite
* Multiple Conditions:: stacking blockers
Extending Edna
* Naming Conventions:: How to name new functions
* Finders: Finders (1). Making a new finder
* Actions: Actions (1). Making a new action
* Conditions: Conditions (1). Making a new condition
Contributing
* Bugs::
* Working with EDE:: And all its quirks
* Compiling Edna:: How to compile Edna
* Testing Edna:: Ensuring Edna works the way we think she will
* Before Sending Changes:: Follow these instructions before sending us anything
* Developing with Bazaar:: How to use this strange VCS
* Documentation:: Improving the documentation
Changelog
* 1.0.2: 102.
* 1.0.1: 101.
* 1.0: 10.
* 1.0beta8: 10beta8.
* 1.0beta7: 10beta7.
* 1.0beta6: 10beta6.
* 1.0beta5: 10beta5.
* 1.0beta4: 10beta4.
* 1.0beta3: 10beta3.
* 1.0beta2: 10beta2.
File: org-edna.info, Node: Copying, Next: Introduction, Prev: Top, Up: Top
Copying
*******
Copyright (C) 2017-2018 Free Software Foundation, Inc.
This program is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see
.
File: org-edna.info, Node: Introduction, Next: Basic Features, Prev: Copying, Up: Top
Introduction
************
Extensible Dependencies ’N’ Actions (EDNA) for Org Mode tasks
Edna provides an extensible means of specifying conditions which must
be fulfilled before a task can be completed and actions to take once it
is.
Org Edna runs when either the BLOCKER or TRIGGER properties are set
on a heading, and when it is changing from a TODO state to a DONE state.
For brevity, we use TODO state to indicate any state in
‘org-not-done-keywords’, and DONE state to indicate any state in
‘org-done-keywords’.
* Menu:
* Installation and Setup:: How to install Edna
* Basic Operation:: How to use Edna
* Blockers:: Blocking a TODO Item
* Triggers:: Triggering actions after completing a task
* Syntax:: Basic explanation of Edna’s syntax
File: org-edna.info, Node: Installation and Setup, Next: Basic Operation, Up: Introduction
Installation and Setup
======================
*Requirements*
Emacs 25.1
seq 2.19
org 9.0.5
There are two ways to install Edna: From GNU ELPA, or from source.
From ELPA:
M-x package-install org-edna
From Source:
bzr branch https://bzr.savannah.gnu.org/r/org-edna-el/ org-edna
After that, add the following to your init file (typically .emacs):
;; Only necessary if installing from source
(add-to-list 'load-path "/full/path/to/org-edna/")
(require 'org-edna)
;; Always necessary
(org-edna-load)
If you ever want to disable Edna, run ‘org-edna-unload’.
File: org-edna.info, Node: Basic Operation, Next: Blockers, Prev: Installation and Setup, Up: Introduction
Basic Operation
===============
Let’s start with an example: Say you want to do laundry, but once you’ve
put your clothes in the washer, you forget about it. Even with a tool
like org-notify or appt, Org won’t know when to remind you. If you’ve
got them scheduled for an hour after the other, maybe you forgot one
time, or ran a little late. Now Org will remind you too early.
Edna can handle this for you like so:
* TODO Put clothes in washer
SCHEDULED: <2017-04-08 Sat 09:00>
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:END:
* TODO Put clothes in dryer
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
* TODO Fold laundry
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
* TODO Put clothes away
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
After you’ve put your clothes in the washer and mark the task DONE,
Edna will schedule the following task for one hour after you set the
first heading as done.
Another example might be a checklist that you’ve done so many times
that you do part of it on autopilot:
* TODO Address all TODOs in code
* TODO Commit Code to Repository
The last thing anyone wants is to find out that some part of the code
on which they’ve been working for days has a surprise waiting for them.
Once again, Edna can help:
* TODO Address all TODOs in code
:PROPERTIES:
:BLOCKER: file("main.cpp") file("code.cpp") re-search?("TODO")
:END:
* TODO Commit Code to Repository
File: org-edna.info, Node: Blockers, Next: Triggers, Prev: Basic Operation, Up: Introduction
Blockers
========
A blocker indicates conditions which must be met in order for a heading
to be marked as DONE. Typically, this will be a list of headings that
must be marked as DONE.
File: org-edna.info, Node: Triggers, Next: Syntax, Prev: Blockers, Up: Introduction
Triggers
========
A trigger is an action to take when a heading is set to done. For
example, scheduling another task, marking another task as TODO, or
renaming a file.
File: org-edna.info, Node: Syntax, Prev: Triggers, Up: Introduction
Syntax
======
Edna has its own language for commands, the basic form of which is
KEYWORD(ARG1 ARG2 ...)
KEYWORD can be any valid lisp symbol, such as key-word, KEY_WORD!, or
keyword?.
Each argument can be one of the following:
• A symbol, such as arg or org-mode
• A quoted string, such as “hello” or “My name is Edna”
• A number, such as 0.5, +1e3, or -5
• A UUID, such as c5e30c76-879a-494d-9281-3a4b559c1a3c
Each argument takes specific datatypes as input, so be sure to read
the entry before using it.
The parentheses can be omitted for commands with no arguments.
File: org-edna.info, Node: Basic Features, Next: Advanced Features, Prev: Introduction, Up: Top
Basic Features
**************
The most basic features of Edna are *finders* and *actions*.
* Menu:
* Finders:: How to find targets
* Actions:: Next steps
* Getting Help:: Getting some help
File: org-edna.info, Node: Finders, Next: Actions, Up: Basic Features
Finders
=======
A finder specifies locations from which to test conditions or perform
actions. These locations are referred to as “targets”. The current
heading, i.e. the one that is being blocked or triggered, is referred
to as the “source” heading.
More than one finder may be used. In this case, the targets are
merged together, removing any duplicates.
Many finders take additional options, marked “OPTIONS”. See *note
relatives:: for information on these options.
* Menu:
* ancestors:: Find a list of ancestors
* children:: Find all immediate children
* descendants:: Find all descendants
* file:: Find a file by name
* first-child:: Find the first child of a heading
* ids:: Find a list of headings with given IDs
* match:: Good old tag matching
* next-sibling:: Find the next sibling
* next-sibling-wrap:: Find the next sibling, wrapping around
* olp:: Find a heading by its outline path
* org-file:: Find a file in org-directory
* parent:: Find a parent
* previous-sibling:: Find the previous sibling
* previous-sibling-wrap:: Find the previous sibling, with wrapping
* relatives:: Generic relative finder
* rest-of-siblings:: Find the remaining siblings
* rest-of-siblings-wrap:: Find the remaining siblings, with wrapping
* self::
* siblings:: Find all the siblings
* siblings-wrap:: Find the siblings, but wrap around
File: org-edna.info, Node: ancestors, Next: children, Up: Finders
ancestors
---------
• Syntax: ancestors(OPTIONS...)
The ‘ancestors’ finder returns a list of the source heading’s
ancestors.
For example:
* TODO Heading 1
** TODO Heading 2
** TODO Heading 3
*** TODO Heading 4
**** TODO Heading 5
:PROPERTIES:
:BLOCKER: ancestors
:END:
In the above example, “Heading 5” will be blocked until “Heading 1”,
“Heading 3”, and “Heading 4” are marked “DONE”, while “Heading 2” is
ignored.
File: org-edna.info, Node: children, Next: descendants, Prev: ancestors, Up: Finders
children
--------
• Syntax: children(OPTIONS...)
The ‘children’ finder returns a list of the *immediate* children of
the source heading. If the source has no children, no target is
returned.
In order to get all levels of children of the source heading, use the
*note descendants:: keyword instead.
File: org-edna.info, Node: descendants, Next: file, Prev: children, Up: Finders
descendants
-----------
• Syntax: descendants(OPTIONS...)
The ‘descendants’ finder returns a list of all descendants of the
source heading.
* TODO Heading 1
:PROPERTIES:
:BLOCKER: descendants
:END:
** TODO Heading 2
*** TODO Heading 3
**** TODO Heading 4
***** TODO Heading 5
In the above example, “Heading 1” will block until Headings 2, 3, 4,
and 5 are DONE.
File: org-edna.info, Node: file, Next: first-child, Prev: descendants, Up: Finders
file
----
• Syntax: file(“FILE”)
The ‘file’ finder finds a single file, specified as a string. The
returned target will be the minimum point in the file.
Note that this does not give a valid heading, so any conditions or
actions that require will throw an error. Consult the documentation for
individual actions or conditions to determine which ones will and won’t
work.
See *note conditions: Conditions. for how to set a different
condition. For example:
* TODO Test
:PROPERTIES:
:BLOCKER: file("~/myfile.org") headings?
:END:
Here, “Test” will block until myfile.org is clear of headings.
File: org-edna.info, Node: first-child, Next: ids, Prev: file, Up: Finders
first-child
-----------
• Syntax: first-child(OPTIONS...)
Return the first child of the source heading. If the source heading
has no children, no target is returned.
File: org-edna.info, Node: ids, Next: match, Prev: first-child, Up: Finders
ids
---
• Syntax: id(ID1 ID2 ...)
The ‘ids’ finder will search for headings with given IDs, using
‘org-id’. Any number of UUIDs may be specified. For example:
* TODO Test
:PROPERTIES:
:BLOCKER: ids(62209a9a-c63b-45ef-b8a8-12e47a9ceed9 6dbd7921-a25c-4e20-b035-365677e00f30)
:END:
Here, “Test” will block until the heading with ID
62209a9a-c63b-45ef-b8a8-12e47a9ceed9 and the heading with ID
6dbd7921-a25c-4e20-b035-365677e00f30 are set to “DONE”.
Note that UUIDs need not be quoted; Edna will handle that for you.
File: org-edna.info, Node: match, Next: next-sibling, Prev: ids, Up: Finders
match
-----
• Syntax: match(“MATCH-STRING” SCOPE SKIP)
The ‘match’ keyword will take any arguments that ‘org-map-entries’
usually takes. In fact, the arguments to ‘match’ are passed straight
into ‘org-map-entries’.
* TODO Test
:PROPERTIES:
:BLOCKER: match("test&mine" agenda)
:END:
“Test” will block until all entries tagged “test” and “mine” in the
agenda files are marked DONE.
See the documentation for ‘org-map-entries’ for a full explanation of
the first argument.
File: org-edna.info, Node: next-sibling, Next: next-sibling-wrap, Prev: match, Up: Finders
next-sibling
------------
• Syntax: next-sibling(OPTIONS...)
The ‘next-sibling’ keyword returns the next sibling of the source
heading, if any.
File: org-edna.info, Node: next-sibling-wrap, Next: olp, Prev: next-sibling, Up: Finders
next-sibling-wrap
-----------------
• Syntax: next-sibling-wrap(OPTIONS...)
Find the next sibling of the source heading, if any. If there isn’t,
wrap back around to the first heading in the same subtree.
File: org-edna.info, Node: olp, Next: org-file, Prev: next-sibling-wrap, Up: Finders
olp
---
• Syntax: olp(“FILE” “OLP”)
Finds the heading given by OLP in FILE. Both arguments are strings.
* TODO Test
:PROPERTIES:
:BLOCKER: olp("test.org" "path/to/heading")
:END:
“Test” will block if the heading “path/to/heading” in “test.org” is
not DONE.
File: org-edna.info, Node: org-file, Next: parent, Prev: olp, Up: Finders
org-file
--------
• Syntax: org-file(“FILE”)
A special form of ‘file’, ‘org-file’ will find FILE in
‘org-directory’.
FILE is the relative path of a file in ‘org-directory’. Nested files
are allowed, such as “my-directory/my-file.org”. The returned target is
the minimum point of FILE.
* TODO Test
:PROPERTIES:
:BLOCKER: org-file("test.org")
:END:
Note that the file still requires an extension; the “org” here just
means to look in ‘org-directory’, not necessarily an Org mode file.
File: org-edna.info, Node: parent, Next: previous-sibling, Prev: org-file, Up: Finders
parent
------
• Syntax: parent(OPTIONS...)
Returns the parent of the source heading, if any.
File: org-edna.info, Node: previous-sibling, Next: previous-sibling-wrap, Prev: parent, Up: Finders
previous-sibling
----------------
• Syntax: previous-sibling(OPTIONS...)
Returns the previous sibling of the source heading on the same level.
File: org-edna.info, Node: previous-sibling-wrap, Next: relatives, Prev: previous-sibling, Up: Finders
previous-sibling-wrap
---------------------
• Syntax: previous-sibling-wrap(OPTIONS...)
Returns the previous sibling of the source heading on the same level.
File: org-edna.info, Node: relatives, Next: rest-of-siblings, Prev: previous-sibling-wrap, Up: Finders
relatives
---------
Find some relative of the current heading.
• Syntax: relatives(OPTION OPTION...)
• Syntax: chain-find(OPTION OPTION...)
Identical to the chain argument in org-depend, relatives selects its
single target using the following method:
1. Creates a list of possible targets
2. Filters the targets from Step 1
3. Sorts the targets from Step 2
One option from each of the following three categories may be used;
if more than one is specified, the last will be used. Filtering is the
exception to this; each filter argument adds to the current filter.
Apart from that, argument order is irrelevant.
The chain-find finder is also provided for backwards compatibility,
and for similarity to org-depend.
All arguments are symbols, unless noted otherwise.
*Selection*
• from-top: Select siblings of the current heading, starting at the
top
• from-bottom: As above, but from the bottom
• from-current: Selects siblings, starting from the heading (wraps)
• no-wrap: As above, but without wrapping
• forward-no-wrap: Find entries on the same level, going forward
• forward-wrap: As above, but wrap when the end is reached
• backward-no-wrap: Find entries on the same level, going backward
• backward-wrap: As above, but wrap when the start is reached
• walk-up: Walk up the tree, excluding self
• walk-up-with-self: As above, but including self
• walk-down: Recursively walk down the tree, excluding self
• walk-down-with-self: As above, but including self
• step-down: Collect headings from one level down
*Filtering*
• todo-only: Select only targets with TODO state set that isn’t a
DONE state
• todo-and-done-only: Select all targets with a TODO state set
• no-comments: Skip commented headings
• no-archive: Skip archived headings
• NUMBER: Only use that many headings, starting from the first one If
passed 0, use all headings If <0, omit that many headings from the
end
• “+tag”: Only select headings with given tag
• “-tag”: Only select headings without tag
• “REGEX”: select headings whose titles match REGEX
*Sorting*
• no-sort: Remove other sorting in affect
• reverse-sort: Reverse other sorts (stacks with other sort methods)
• random-sort: Sort in a random order
• priority-up: Sort by priority, highest first
• priority-down: Same, but lowest first
• effort-up: Sort by effort, highest first
• effort-down: Sort by effort, lowest first
• scheduled-up: Scheduled time, farthest first
• scheduled-down: Scheduled time, closest first
• deadline-up: Deadline time, farthest first
• deadline-down: Deadline time, closest first
• timestamp-up: Timestamp time, farthest first
• timestamp-down: Timestamp time, closest first
Many of the other finders are shorthand for argument combinations of
relative:
*note ancestors::
walk-up
*note children::
step-down
*note descendants::
walk-down
*note first-child::
step-down 1
*note next-sibling::
forward-no-wrap 1
*note next-sibling-wrap::
forward-wrap 1
*note parent::
walk-up 1
*note previous-sibling::
backward-no-wrap 1
*note previous-sibling-wrap::
backward-wrap 1
*note rest-of-siblings::
forward-no-wrap
*note rest-of-siblings-wrap::
forward-wrap
*note siblings::
from-top
*note siblings-wrap::
forward-wrap
Because these are implemented as shorthand, any arguments for
relatives may also be passed to one of these finders.
File: org-edna.info, Node: rest-of-siblings, Next: rest-of-siblings-wrap, Prev: relatives, Up: Finders
rest-of-siblings
----------------
• Syntax: rest-of-siblings(OPTIONS...)
Starting from the heading following the current one, all same-level
siblings are returned.
File: org-edna.info, Node: rest-of-siblings-wrap, Next: self, Prev: rest-of-siblings, Up: Finders
rest-of-siblings-wrap
---------------------
• Syntax: rest-of-siblings-wrap(OPTIONS...)
Starting from the heading following the current one, all same-level
siblings are returned. When the end is reached, wrap back to the
beginning.
File: org-edna.info, Node: self, Next: siblings, Prev: rest-of-siblings-wrap, Up: Finders
self
----
• Syntax: self
Returns the source heading.
File: org-edna.info, Node: siblings, Next: siblings-wrap, Prev: self, Up: Finders
siblings
--------
• Syntax: siblings(OPTIONS...)
Returns all siblings of the source heading as targets, starting from
the first sibling.
File: org-edna.info, Node: siblings-wrap, Prev: siblings, Up: Finders
siblings-wrap
-------------
• Syntax: siblings-wrap(OPTIONS...)
Finds the siblings on the same level as the source heading, wrapping
when it reaches the end.
Identical to the *note rest-of-siblings-wrap:: finder.
File: org-edna.info, Node: Actions, Next: Getting Help, Prev: Finders, Up: Basic Features
Actions
=======
Once Edna has collected its targets for a trigger, it will perform
actions on them.
Actions must always end with ’!’.
* Menu:
* Scheduled/Deadline:: Operate on planning information
* TODO State:: Set todo state
* Archive:: Archive targets
* Chain Property:: Copy properties from source to targets
* Clocking:: Clock in or out of a target
* Property:: Set properties of targets
* Priority:: Set priorities of targets
* Tag:: Tags of a target
* Effort:: So much effort!
File: org-edna.info, Node: Scheduled/Deadline, Next: TODO State, Up: Actions
Scheduled/Deadline
------------------
• Syntax: scheduled!(OPTIONS)
• Syntax: deadline!(OPTIONS)
Set the scheduled or deadline time of any target headings.
There are several forms that the planning keywords can take. In the
following, PLANNING is either scheduled or deadline.
• PLANNING!(“DATE[ TIME]”)
Sets PLANNING to DATE at TIME. If DATE is a weekday instead of a
date, then set PLANNING to the following weekday. If TIME is not
specified, only a date will be added to the target.
Any string recognized by ‘org-read-date’ may be used for DATE.
TIME is a time string, such as HH:MM.
• PLANNING!(rm|remove)
Remove PLANNING from all targets. The argument to this form may be
either a string or a symbol.
• PLANNING!(copy|cp)
Copy PLANNING info verbatim from the source heading to all targets.
The argument to this form may be either a string or a symbol.
• PLANNING!(“[+|-|++|–]NTHING[ [+|-]LANDING]”)
Increment(+) or decrement(-) target’s PLANNING by N THINGs relative
to either itself (+/-) or the current time (++/–).
N is an integer
THING is one of y (years), m (months), d (days), h (hours), M
(minutes), a (case-insensitive) day of the week or its
abbreviation, or the strings “weekday” or “wkdy”.
If a day of the week is given as THING, move forward or backward N
weeks to find that day of the week.
If one of “weekday” or “wkdy” is given as THING, move forward or
backward N days, moving forward or backward to the next weekday.
This form may also include a “landing” specifier to control where
in the week the final date lands. LANDING may be one of the
following:
• A day of the week, which means adjust the final date forward
(+) or backward (-) to land on that day of the week.
• One of “weekday” or “wkdy”, which means adjust the target date
to the closest weekday.
• One of “weekend” or “wknd”, which means adjust the target date
to the closest weekend.
• PLANNING!(“float [+|-|++|–]N DAYNAME[ MONTH[ DAY]]”)
Set time to the date of the Nth DAYNAME before/after MONTH DAY, as
per ‘diary-float’.
N is an integer.
DAYNAME may be either an integer, where 0=Sunday, 1=Monday, etc.,
or a string for that day.
MONTH may be an integer, 1-12, or a month’s string. If MONTH is
empty, the following (+) or previous (-) month relative to the
target’s time (+/-) or the current time (++/–).
DAY is an integer, or empty or 0 to use the first of the month (+)
or the last of the month (-).
Examples:
scheduled!(“Mon 09:00”)
Set SCHEDULED to the following Monday at 9:00
deadline!(“++2h”)
Set DEADLINE to two hours from now.
deadline!(copy) deadline!(“+1h”)
Copy the source deadline to the target, then increment it by an
hour.
scheduled!(“+1wkdy”)
Set SCHEDULED to the next weekday
scheduled!(“+1d +wkdy”)
Same as above
deadline!(“+1m -wkdy”)
Set DEADLINE up one month, but move backward to find a weekend
scheduled!(“float 2 Tue Feb”)
Set SCHEDULED to the second Tuesday in the following February
scheduled!(“float 3 Thu”)
Set SCHEDULED to the third Thursday in the following month
* Menu:
* Timestamp Format:: Specifying a timestamp format
File: org-edna.info, Node: Timestamp Format, Up: Scheduled/Deadline
Timestamp Format
................
When using one of the planning modifiers, it isn’t always possible to
deduce how the timestamp format will be chosen if using ++ or –. The
following method is used:
1. If the target heading already has a timestamp, that format is used.
2. If the modifier with the ++ or – is “h” or “M” (hours or minutes),
long format (includes time) is used.
3. If the property ‘EDNA_TS_FORMAT’ is set on the target heading, its
value will be used. It should be either ‘long’ for long format
(includes time) or ‘short’ for short format (does not include
time).
4. The user variable ‘org-edna-timestamp-format’ is the final
fallback. It should be either the symbol ‘long’ or ‘short’. It
defaults to ‘short’.
File: org-edna.info, Node: TODO State, Next: Archive, Prev: Scheduled/Deadline, Up: Actions
TODO State
----------
• Syntax: todo!(NEW-STATE)
Sets the TODO state of the target heading to NEW-STATE.
NEW-STATE may either be a string or a symbol denoting the new TODO
state. It can also be the empty string, in which case the TODO state is
removed.
Example:
* TODO Heading 1
:PROPERTIES:
:TRIGGER: next-sibling todo!(DONE)
:END:
* TODO Heading 2
In this example, when “Heading 1” is marked as DONE, it will also
mark “Heading 2” as DONE:
* DONE Heading 1
:PROPERTIES:
:TRIGGER: next-sibling todo!(DONE)
:END:
* DONE Heading 2
File: org-edna.info, Node: Archive, Next: Chain Property, Prev: TODO State, Up: Actions
Archive
-------
• Syntax: archive!
Archives all targets with confirmation.
Confirmation is controlled with ‘org-edna-prompt-for-archive’. If
this option is nil, Edna will not ask before archiving targets.
File: org-edna.info, Node: Chain Property, Next: Clocking, Prev: Archive, Up: Actions
Chain Property
--------------
• Syntax: chain!(“PROPERTY”)
Copies PROPERTY from the source entry to all targets. Does nothing
if the source heading has no property PROPERTY.
Example:
* TODO Heading 1
:PROPERTIES:
:COUNTER: 2
:TRIGGER: next-sibling chain!("COUNTER")
:END:
* TODO Heading 2
In this example, when “Heading 1” is marked as DONE, it will copy its
COUNTER property to “Heading 2”:
* DONE Heading 1
:PROPERTIES:
:COUNTER: 2
:TRIGGER: next-sibling chain!("COUNTER")
:END:
* TODO Heading 2
:PROPERTIES:
:COUNTER: 2
:END:
File: org-edna.info, Node: Clocking, Next: Property, Prev: Chain Property, Up: Actions
Clocking
--------
• Syntax: clock-in!
• Syntax: clock-out!
Clocks into or out of all targets.
‘clock-in!’ has no special handling of targets, so be careful when
specifying multiple targets.
In contrast, ‘clock-out!’ ignores its targets and only clocks out of
the current clock, if any.
File: org-edna.info, Node: Property, Next: Priority, Prev: Clocking, Up: Actions
Property
--------
• Syntax: set-property!(“PROPERTY” “VALUE”)
• Syntax: set-property!(“PROPERTY” inc)
• Syntax: set-property!(“PROPERTY” dec)
• Syntax: set-property!(“PROPERTY” next)
• Syntax: set-property!(“PROPERTY” prev)
• Syntax: set-property!(“PROPERTY” previous)
The first form sets the property PROPERTY on all targets to VALUE.
If VALUE is a symbol, it is interpreted as follows:
inc
Increment a numeric property value by one
dec
Decrement a numeric property value by one
If either ‘inc’ or ‘dec’ attempt to modify a non-numeric property
value, Edna will fail with an error message.
next
Cycle the property through to the next allowed property value
previous
Cycle the property through to the previous allowed property value
The symbol ‘prev’ may be used as an abbreviation for ‘previous’.
Similar to ‘inc’ and ‘dec’, any of these will fail if there are no
defined properties. When reaching the end of the list of allowed
properties, ‘next’ will cycle back to the beginning.
Example:
#+PROPERTY: TEST_ALL a b c d
* TODO Test Heading
:PROPERTIES:
:TEST: d
:TRIGGER: self set-property!("TEST" next)
:END:
When “Test Heading” is set to DONE, its TEST property will change to
“a”. This also works with ‘previous’, but in the opposite direction.
Additionally, all special forms will fail if the property is not
already set:
* TODO Test
:PROPERTIES:
:TRIGGER: self set-property!("TEST" inc)
:END:
In the above example, if “Test” is set to DONE, Edna will fail to
increment the TEST property, since it doesn’t exist.
• Syntax: delete-property!(“PROPERTY”)
Deletes the property PROPERTY from all targets.
Examples:
set-property!(“COUNTER” “1”)
Sets the property COUNTER to 1 on all targets
set-property!(“COUNTER” inc)
Increments the property COUNTER by 1. Following the previous
example, it would be 2.
File: org-edna.info, Node: Priority, Next: Tag, Prev: Property, Up: Actions
Priority
--------
Sets the priority of all targets.
• Syntax: set-priority!(“PRIORITY”)
Set the priority to the first character of PRIORITY.
• Syntax: set-priority!(up)
Cycle the target’s priority up through the list of allowed
priorities.
• Syntax: set-priority!(down)
Cycle the target’s priority down through the list of allowed
priorities.
• Syntax: set-priority!(P)
Set the target’s priority to the character P.
File: org-edna.info, Node: Tag, Next: Effort, Prev: Priority, Up: Actions
Tag
---
• Syntax: tag!(“TAG-SPEC”)
Tags all targets with TAG-SPEC, which is any valid tag specification,
e.g. tag1:tag2
File: org-edna.info, Node: Effort, Prev: Tag, Up: Actions
Effort
------
Modifies the effort of all targets.
• Syntax: set-effort!(“VALUE”)
Set the effort of all targets to “VALUE”.
• Syntax: set-effort!(NUMBER)
Sets the effort to the NUMBER’th allowed effort property.
• Syntax: set-effort!(increment)
Increment the effort value.
File: org-edna.info, Node: Getting Help, Prev: Actions, Up: Basic Features
Getting Help
============
Edna provides help for any keyword with ‘M-x org-edna-describe-keyword’.
When invoked, a list of keywords (finders, actions, etc.) known to Edna
will be provided. Select any one to get its description.
This description includes the syntax and an explanation of what the
keyword does. Some descriptions also contain examples.
File: org-edna.info, Node: Advanced Features, Next: Extending Edna, Prev: Basic Features, Up: Top
Advanced Features
*****************
* Menu:
* Finder Cache:: Making the finders work faster
* Conditions:: More than just DONE headings
* Consideration:: Only some of them
* Conditional Forms:: If/Then/Else
* Setting the Properties:: The easy way to set BLOCKER and TRIGGER
File: org-edna.info, Node: Finder Cache, Next: Conditions, Up: Advanced Features
Finder Cache
============
Some finders, ‘match’ in particular, can take a long time to run.
Oftentimes, this can make it unappealing to use Edna at all, especially
with long checklists.
The finder cache is one solution to this. To enable it, set
‘org-edna-finder-use-cache’ to non-nil. This can be done through the
customization interface, or manually with ‘setq’.
When enabled, the cache will store the results of every finder form
for a configurable amount of time. This timeout is controlled by
‘org-edna-finder-cache-timeout’. The cache is also invalidated if any
of the results are invalid, which can happen if their target files have
been closed.
For example, if there are several entries in a checklist that all use
the form ‘match("daily")’ as part of their trigger, the results of that
form will be cached. When the next item is marked as DONE, the results
will be searched for in cache, not recomputed.
When reverting Org mode files, the cache will often be invalidated.
This isn’t the case for every Org mode file, so we can’t just tell Emacs
to automatically reset the cache when reverting a file. Instead, we
provide the command ‘org-edna-reset-cache’ to reset the finder cache.
If you notice headings that should be blocking but aren’t while cache is
enabled, reset the cache and check again.
File: org-edna.info, Node: Conditions, Next: Consideration, Prev: Finder Cache, Up: Advanced Features
Conditions
==========
Edna gives you he option to specify *blocking conditions*. Each
condition is checked for each of the specified targets; if one of the
conditions returns true for that target, then the source heading is
blocked.
If no condition is specified, ‘!done?’ is used by default, which
means block if any target heading isn’t done.
* Menu:
* Heading is DONE::
* File Has Headings::
* Heading TODO State::
* Lisp Variable Set::
* Heading Has Property::
* Regexp Search:: Search for a regular expression
* Checking Tags:: Matching against a set of tags
* Matching Headings:: Matching against a match string
* Negating Conditions:: Doing the opposite
* Multiple Conditions:: stacking blockers
File: org-edna.info, Node: Heading is DONE, Next: File Has Headings, Up: Conditions
Heading is DONE
---------------
• Syntax: done?
Blocks the source heading if any target heading is DONE.
File: org-edna.info, Node: File Has Headings, Next: Heading TODO State, Prev: Heading is DONE, Up: Conditions
File Has Headings
-----------------
• Syntax: headings?
Blocks the source heading if any target belongs to a file that has an
Org heading. This means that target does not have to be a heading.
org-file("refile.org") headings?
The above example blocks if refile.org has any headings.
File: org-edna.info, Node: Heading TODO State, Next: Lisp Variable Set, Prev: File Has Headings, Up: Conditions
Heading TODO State
------------------
• Syntax: todo-state?(STATE)
Blocks if any target heading has TODO state set to STATE.
STATE may be a string or a symbol.
File: org-edna.info, Node: Lisp Variable Set, Next: Heading Has Property, Prev: Heading TODO State, Up: Conditions
Lisp Variable Set
-----------------
• Syntax: variable-set?(VARIABLE VALUE)
Evaluate VARIABLE when visiting a target, and compare it with ‘equal’
against VALUE. Block the source heading if VARIABLE = VALUE.
VARIABLE should be a symbol, and VALUE is any valid lisp expression.
Examples:
self variable-set?(test-variable 12)
Blocks if the variable ‘test-variable’ is set to 12.
self variable-set?(buffer-file-name “org-edna.org”)
Blocks if the variable ‘buffer-file-name’ is set to “org-edna.org”.
File: org-edna.info, Node: Heading Has Property, Next: Regexp Search, Prev: Lisp Variable Set, Up: Conditions
Heading Has Property
--------------------
• Syntax: has-property?(“PROPERTY” “VALUE”)
Tests each target for the property PROPERTY, and blocks if it’s set
to VALUE.
Example:
* TODO Take Shower
:PROPERTIES:
:COUNT: 1
:TRIGGER: self set-property!("COUNT" inc) todo!("TODO")
:END:
* TODO Wash Towels
:PROPERTIES:
:BLOCKER: previous-sibling !has-property?("COUNT" "3")
:TRIGGER: previous-sibling set-property!("COUNT" "0")
:END:
In this example, “Wash Towels” can’t be completed until the user has
showered at least three times.
File: org-edna.info, Node: Regexp Search, Next: Checking Tags, Prev: Heading Has Property, Up: Conditions
Regexp Search
-------------
• Syntax: re-search?(“REGEXP”)
Blocks the source heading if the regular expression REGEXP is present
in any of the targets.
The targets are expected to be files, although this will work with
other targets as well. When given a target heading, the heading’s file
will be searched.
File: org-edna.info, Node: Checking Tags, Next: Matching Headings, Prev: Regexp Search, Up: Conditions
Checking Tags
-------------
• Syntax: has-tags?(“TAG1” “TAG2” ...)
Blocks the source heading if any of the target headings have one or
more of the given tags.
* TODO Task 1 :tag1:
* TODO Task 2 :tag3:tag2:
* TODO Task 3
:PROPERTIES:
:BLOCKER: rest-of-siblings-wrap has-tags?("tag1" "tag2")
:END:
In the above example, Tasks 1 and 2 will block Task 3. Task 1 will
block it because it contains “tag1” as one of its tags, and likewise for
Task 2 and “tag2”.
Note that marking “Task 1” or “Task 2” as DONE will not unblock “Task
3”. If you want to set up such a system, use the *note match:: finder.
File: org-edna.info, Node: Matching Headings, Next: Negating Conditions, Prev: Checking Tags, Up: Conditions
Matching Headings
-----------------
• Syntax: matches?(“MATCH-STRING”)
Blocks the source heading if any of the target headings match against
MATCH-STRING.
MATCH-STRING is a string passed to ‘org-map-entries’.
* TODO Task 1
* TODO Task 2
* TODO Task 3
:PROPERTIES:
:BLOCKER: rest-of-siblings-wrap !matches?("TODO==\"DONE\"")
:END:
In the above example, Tasks 1 and 2 will block Task 3 until they’re
marked as DONE.
File: org-edna.info, Node: Negating Conditions, Next: Multiple Conditions, Prev: Matching Headings, Up: Conditions
Negating Conditions
-------------------
Any condition can be negated by using ’!’ before the condition.
match("test") !has-property?("PROP" "1")
The above example will cause the source heading to block if any
heading tagged “test” does *not* have the property PROP set to “1”.
File: org-edna.info, Node: Multiple Conditions, Prev: Negating Conditions, Up: Conditions
Multiple Conditions
-------------------
Multiple blocking conditions can be used for a single entry. The
heading will block if any of the provided conditions evaluate to true.
* TODO Heading 1
:PROPERTIES:
:ID: 1942caf2-caad-4757-b689-3c0029c1d8a5
:END:
* TODO Heading 2
* TODO Heading 3
:PROPERTIES:
:BLOCKER: previous-sibling !done? ids(1942caf2-caad-4757-b689-3c0029c1d8a5) !done?
:END:
“Heading 3” will block if either “Heading 1” isn’t done (ids) or
“Heading 2” isn’t done (previous-sibling).
File: org-edna.info, Node: Consideration, Next: Conditional Forms, Prev: Conditions, Up: Advanced Features
Consideration
=============
“Consideration” and “consider” are special keywords that are only valid
for blockers.
A blocker says “If ANY heading in TARGETS meets CONDITION, block this
task”.
In order to modify the ANY part of that statement, the ‘consider’
keyword may be used:
1. consider(any)
2. consider(all)
3. consider(FRACTION)
4. consider(NUMBER)
(1) blocks the current task if any target meets the blocking
condition. This is the default case.
(2) blocks the current task only if all targets meet the blocking
condition.
* Shovel Snow
** TODO Shovel on Monday
** TODO Shovel on Tuesday
** TODO Shovel on Wednesday
** TODO Put shovel away
:PROPERTIES:
:BLOCKER: consider(all) rest-of-siblings-wrap
:END:
The above example blocks “Put shovel away” so long as all of the
siblings are still marked TODO.
(3) blocks the current task if at least FRACTION of the targets meet
the blocking condition.
* Work
** TODO Shovel Snow
** TODO Clean room
** TODO Vacuum
** TODO Eat lunch
** TODO Work on Edna
:PROPERTIES:
:BLOCKER: consider(0.5) rest-of-siblings-wrap
:END:
The above example blocks “Work on Edna” so long as at least half of
the siblings are marked TODO. This means that three of them must be
completed before development can begin on Edna.
(4) blocks the current task if at least NUMBER of the targets meet
the blocking condition.
* Work
** TODO Shovel Snow
** TODO Clean room
** TODO Vacuum
** TODO Eat lunch
** TODO Work on Edna
:PROPERTIES:
:BLOCKER: consider(2) rest-of-siblings-wrap
:END:
The above example blocks “Work on Edna” so long as two of the
siblings are marked TODO. This means that NUMBER=1 is the same as
specifying ‘any’.
A consideration must be specified before the conditions to which it
applies.
Both “consider” and “consideration” are valid keywords; they both
mean the same thing.
File: org-edna.info, Node: Conditional Forms, Next: Setting the Properties, Prev: Consideration, Up: Advanced Features
Conditional Forms
=================
Let’s say you’ve got the following checklist:
* TODO Nightly
DEADLINE: <2017-12-22 Fri 22:00 +1d>
:PROPERTIES:
:ID: 12345
:BLOCKER: match("nightly")
:TRIGGER: match("nightly") todo!(TODO)
:END:
* TODO Prepare Tomorrow's Lunch :nightly:
* TODO Lock Back Door :nightly:
* TODO Feed Dog :nightly:
You don’t know in what order you want to perform each task, nor
should it matter. However, you also want the parent heading, “Nightly”,
to be marked as DONE when you’re finished with the last task.
There are two solutions to this: 1. Have each task attempt to mark
“Nightly” as DONE, which will spam blocking messages after each task.
The second is to use conditional forms. Conditional forms are
simple; it’s just if/then/else/endif:
if CONDITION then THEN else ELSE endif
Here’s how that reads:
“If CONDITION would not block, execute THEN. Otherwise, execute
ELSE.”
For our nightly entries, this looks as follows:
* TODO Prepare Tomorrow's Lunch :nightly:
:PROPERTIES:
:TRIGGER: if match("nightly") then ids(12345) todo!(DONE) endif
:END:
Thus, we replicate our original blocking condition on all of them, so
it won’t trigger the original until the last one is marked DONE.
Occasionally, you may find that you’d rather execute a form if the
condition *would* block. There are two options.
The first is to use ‘consider(all)’. This will tell Edna to block
only if all of the targets meets the condition, and thus not block if at
least one of them does not meet the condition. This is the opposite of
Edna’s standard operation, which only allows passage if all targets meet
the condition.
* TODO Prepare Tomorrow's Lunch :nightly:
:PROPERTIES:
:TRIGGER: if consider(all) match("nightly") then ids(12345) todo!(DONE) endif
:END:
The second is to switch the then and else clauses:
* TODO Prepare Tomorrow's Lunch :nightly:
:PROPERTIES:
:TRIGGER: if match("nightly") then else ids(12345) todo!(DONE) endif
:END:
The conditional block tells it to evaluate that section. Thus, you
can conditionally add targets, or conditionally check conditions.
File: org-edna.info, Node: Setting the Properties, Prev: Conditional Forms, Up: Advanced Features
Setting the Properties
======================
There are two ways to set the BLOCKER and TRIGGER properties: by hand,
or the easy way. You can probably guess which way we prefer.
With point within the heading you want to edit, type ‘M-x
org-edna-edit’. You end up in a buffer that looks like this:
Edit blockers and triggers in this buffer under their respective sections below.
All lines under a given section will be merged into one when saving back to
the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'.
BLOCKER
BLOCKER STUFF HERE
TRIGGER
TIRGGER STUFF HERE
In here, you can edit the blocker and trigger properties for the
original heading in a cleaner environment. More importantly, you can
complete the names of any valid keyword within the BLOCKER or TRIGGER
sections using ‘completion-at-point’.
When finished, type ‘C-c C-c’ to apply the changes, or ‘C-c C-k’ to
throw out your changes.
File: org-edna.info, Node: Extending Edna, Next: Contributing, Prev: Advanced Features, Up: Top
Extending Edna
**************
Extending Edna is (relatively) simple.
During operation, Edna searches for functions of the form
org-edna-TYPE/KEYWORD.
* Menu:
* Naming Conventions:: How to name new functions
* Finders: Finders (1). Making a new finder
* Actions: Actions (1). Making a new action
* Conditions: Conditions (1). Making a new condition
File: org-edna.info, Node: Naming Conventions, Next: Finders (1), Up: Extending Edna
Naming Conventions
==================
In order to distinguish between actions, finders, and conditions, we add
’?’ to conditions and ’!’ to actions. This is taken from the practice
in Guile and Scheme to suffix destructive functions with ’!’ and
predicates with ’?’.
Thus, one can have an action that files a target, and a finder that
finds a file.
We recommend that you don’t name a finder with a special character at
the end of its name. As we devise new ideas, we consider using special
characters for additional categories of keywords. Thus, to avoid
complications in the future, it’s best if everyone avoids using
characters that may become reserved in the future.
File: org-edna.info, Node: Finders (1), Next: Actions (1), Prev: Naming Conventions, Up: Extending Edna
Finders
=======
Finders have the form org-edna-finder/KEYWORD, like so:
(defun org-edna-finder/test-finder ()
(list (point-marker)))
All finders must return a list of markers, one for each target found,
or nil if no targets were found.
File: org-edna.info, Node: Actions (1), Next: Conditions (1), Prev: Finders (1), Up: Extending Edna
Actions
=======
Actions have the form org-edna-action/KEYWORD!:
(defun org-edna-action/test-action! (last-entry arg1 arg2)
)
Each action has at least one argument: ‘last-entry’. This is a
marker for the current entry (not to be confused with the current
target).
The rest of the arguments are the arguments specified in the form.
File: org-edna.info, Node: Conditions (1), Prev: Actions (1), Up: Extending Edna
Conditions
==========
(defun org-edna-condition/test-cond? (neg))
All conditions have at least one argument, “NEG”. If NEG is non-nil,
the condition should be negated.
Most conditions have the following form:
(defun org-edna-condition/test-condition? (neg)
(let ((condition (my-test-for-condition)))
(when (org-xor condition neg)
(string-for-blocking-entry-here))))
For conditions, we return true if condition is true and neg is false,
or if condition is false and neg is true:
cond neg res
-------------------
t t f
t f t
f t t
f f f
This is an XOR table, so we pass CONDITION and NEG into ‘org-xor’ to
get our result.
A condition must return a string if the current entry should be
blocked.
File: org-edna.info, Node: Contributing, Next: Changelog, Prev: Extending Edna, Up: Top
Contributing
************
We are all happy for any help you may provide.
First, check out the source code on Savannah:
bzr branch https://bzr.savannah.gnu.org/r/org-edna-el/ org-edna
You’ll also want a copy of the most recent Org Mode source:
git clone git://orgmode.org/org-mode.git
* Menu:
* Bugs::
* Working with EDE:: And all its quirks
* Compiling Edna:: How to compile Edna
* Testing Edna:: Ensuring Edna works the way we think she will
* Before Sending Changes:: Follow these instructions before sending us anything
* Developing with Bazaar:: How to use this strange VCS
* Documentation:: Improving the documentation
File: org-edna.info, Node: Bugs, Next: Working with EDE, Up: Contributing
Bugs
====
There are two ways to submit bug reports:
1. Using the bug tracker at Savannah
2. Sending an email using ‘org-edna-submit-bug-report’
When submitting a bug report, be sure to include the Edna form that
caused the bug, with as much context as possible.
File: org-edna.info, Node: Working with EDE, Next: Compiling Edna, Prev: Bugs, Up: Contributing
Working with EDE
================
Our build system uses EDE. EDE can be a little finicky at times, but we
feel the benefits, namely package dependency handling and Makefile
generation, outweigh the costs.
One of the issues that many will likely encounter is the error
“Corrupt file on disk”. This is most often due to EDE not loading all
its subprojects as needed. If you find yourself dealing with this error
often, place the following in your .emacs file:
;; Target types needed for working with edna
(require 'ede/proj-elisp)
(require 'ede/proj-aux)
(require 'ede/proj-misc)
These are the three target types that edna uses: elisp for
compilation and autoloads; aux for auxiliary files such as
documentation; and misc for tests.
When creating a new file, EDE will ask if you want to add it to a
target. Consult with one of the edna devs for guidance, but usually
selecting “none” and letting one of us handle it is a good way to go.
File: org-edna.info, Node: Compiling Edna, Next: Testing Edna, Prev: Working with EDE, Up: Contributing
Compiling Edna
==============
To compile Edna, you’ve got to have EDE create the Makefile first. Run
the following in your Emacs instance to generate the Makefile:
M-x ede-proj-regenerate
This will create the Makefile and point it to the correct version of
Org. The targets are as follows:
compile
Compiles the code. This should be done to verify that everything
will compile, as ELPA requires this.
autoloads
Creates the autoloads file. This should also run without problems,
so it’s a good idea to check this one as well.
check
Runs the tests in ‘org-edna-tests.el’.
To run any target, call ‘make’:
make compile autoloads
The above command compiles Edna and generates the autoloads file.
File: org-edna.info, Node: Testing Edna, Next: Before Sending Changes, Prev: Compiling Edna, Up: Contributing
Testing Edna
============
There are two ways to test Edna: the command-line and through Emacs.
The command-line version is simple, and we ask that you do any final
testing using this method. This is how we periodically check to verify
that new versions of Org mode haven’t broken Edna. It uses the
Makefile, which is generated with EDE. See *note Compiling Edna:: for
how to do that. Once you have, run ‘make check’ on the command line.
Edna tests are written using ‘ERT’, the Emacs Regression Testing
framework. In order to use it interactively in Emacs, the following
must be done:
1. Load ‘org-edna-tests.el’
2. Run ‘M-x ert-run-tests-interactively’
3. Select which tests to run, or just the letter “t” to run all of
them.
Results are printed in their own buffer. See the ERT documentation
for more details.
File: org-edna.info, Node: Before Sending Changes, Next: Developing with Bazaar, Prev: Testing Edna, Up: Contributing
Before Sending Changes
======================
There are a few rules to follow:
• Verify that any new Edna keywords follow the appropriate naming
conventions
• Any new keywords should be documented
• We operate on headings, not headlines
• Use one word in documentation to avoid confusion
• Make sure your changes compile
• Run ’make check’ to verify that your mods don’t break anything
• Avoid additional or altered dependencies if at all possible
• Exception: New versions of Org mode are allowed
File: org-edna.info, Node: Developing with Bazaar, Next: Documentation, Prev: Before Sending Changes, Up: Contributing
Developing with Bazaar
======================
If you’re new to bazaar, we recommend using Emacs’s built-in VC package.
It eases the overhead of dealing with a brand new VCS with a few
standard commands. For more information, see the info page on it (In
Emacs, this is C-h r m Introduction to VC RET).
To contribute with bazaar, you can do the following:
# Hack away and make your changes
$ bzr commit -m "Changes I've made"
$ bzr send -o file-name.txt
Then, use ‘org-edna-submit-bug-report’ and attach “file-name.txt”.
We can then merge that into the main development branch.
File: org-edna.info, Node: Documentation, Prev: Developing with Bazaar, Up: Contributing
Documentation
=============
Documentation is always helpful to us. Please be sure to do the
following after making any changes:
1. Update the info page in the repository with ‘C-c C-e i i’
2. If you’re updating the HTML documentation, switch to a theme that
can easily be read on a white background; we recommend the
“adwaita” theme
File: org-edna.info, Node: Changelog, Prev: Contributing, Up: Top
Changelog
*********
* Menu:
* 1.0.2: 102.
* 1.0.1: 101.
* 1.0: 10.
* 1.0beta8: 10beta8.
* 1.0beta7: 10beta7.
* 1.0beta6: 10beta6.
* 1.0beta5: 10beta5.
* 1.0beta4: 10beta4.
* 1.0beta3: 10beta3.
* 1.0beta2: 10beta2.
File: org-edna.info, Node: 102, Next: 101, Up: Changelog
1.0.2
=====
• Added ‘org-edna-reset-cache’ to allow a user to reset the finder
cache
• Fixed timestamp format bug with scheduled! and deadline!
• See *note Timestamp Format:: for more
File: org-edna.info, Node: 101, Next: 10, Prev: 102, Up: Changelog
1.0.1
=====
• Fixed bug in multiple blocking conditions
File: org-edna.info, Node: 10, Next: 10beta8, Prev: 101, Up: Changelog
1.0
===
• Various bugs fixes
• Fixed parsing of consideration
• Limited cache to just the finders that don’t depend on current
position
• Added “buffer” option for match finder
• Added timestamp sorting to relatives finder
• Inverted meaning of consideration to avoid confusion
• Added *note has-tags?: Checking Tags. and *note matches?: Matching
Headings. conditions
File: org-edna.info, Node: 10beta8, Next: 10beta7, Prev: 10, Up: Changelog
1.0beta8
========
Quick fix for beta7.
File: org-edna.info, Node: 10beta7, Next: 10beta6, Prev: 10beta8, Up: Changelog
1.0beta7
========
Biggest change here is the cache.
• Added cache to the finders to improve performance
• Updated documentation to include EDE
• Added testing and compiling documentation
File: org-edna.info, Node: 10beta6, Next: 10beta5, Prev: 10beta7, Up: Changelog
1.0beta6
========
Lots of parsing fixes.
• Fixed error reporting
• Fixed parsing of negations in conditions
• Fixed parsing of multiple forms inside if/then/else blocks
File: org-edna.info, Node: 10beta5, Next: 10beta4, Prev: 10beta6, Up: Changelog
1.0beta5
========
Some new forms and a new build system.
• Added new forms to set-property!
• Now allows ’inc, ’dec, ’previous, and ’next as values
• Changed build system to EDE to properly handle dependencies
• Fixed compatibility with new Org effort functions
File: org-edna.info, Node: 10beta4, Next: 10beta3, Prev: 10beta5, Up: Changelog
1.0beta4
========
Just some bug fixes from the new form parsing.
• Fixed multiple forms getting incorrect targets
• Fixed multiple forms not evaluating
File: org-edna.info, Node: 10beta3, Next: 10beta2, Prev: 10beta4, Up: Changelog
1.0beta3
========
HUGE addition here
• Conditional Forms
• See *note Conditional Forms:: for more information
• Overhauled Internal Parsing
• Fixed consideration keywords
• Both consider and consideration are accepted now
• Added ’any consideration
• Allows passage if just one target is fulfilled
File: org-edna.info, Node: 10beta2, Prev: 10beta3, Up: Changelog
1.0beta2
========
Big release here, with three new features.
• Added interactive keyword editor with completion
• See *note Setting the Properties:: for how to do that
• New uses of schedule! and deadline!
• New “float” form that mimics diary-float
• New “landing” addition to “+1d” and friends to force planning
changes to land on a certain day or type of day
(weekend/weekday)
• See *note Scheduled/Deadline:: for details
• New “relatives” finder
• Renamed from chain-find with tons of new keywords
• Modified all other relative finders (previous-sibling,
first-child, etc.) to use the same keywords
• See *note relatives:: for details
• New finders
• *note previous-sibling-wrap::
• *note rest-of-siblings-wrap::
Tag Table:
Node: Top225
Node: Copying4453
Node: Introduction5276
Node: Installation and Setup6224
Node: Basic Operation6948
Node: Blockers8799
Node: Triggers9086
Node: Syntax9348
Node: Basic Features10038
Node: Finders10392
Node: ancestors12157
Node: children12751
Node: descendants13161
Node: file13683
Node: first-child14432
Node: ids14692
Node: match15353
Node: next-sibling15991
Node: next-sibling-wrap16248
Node: olp16562
Node: org-file16975
Node: parent17620
Node: previous-sibling17818
Node: previous-sibling-wrap18079
Node: relatives18358
Node: rest-of-siblings22084
Node: rest-of-siblings-wrap22369
Node: self22718
Node: siblings22879
Node: siblings-wrap23116
Node: Actions23420
Node: Scheduled/Deadline24183
Node: Timestamp Format27771
Node: TODO State28659
Node: Archive29384
Node: Chain Property29704
Node: Clocking30457
Node: Property30869
Node: Priority33042
Node: Tag33611
Node: Effort33828
Node: Getting Help34212
Node: Advanced Features34657
Node: Finder Cache35105
Node: Conditions36553
Node: Heading is DONE37438
Node: File Has Headings37644
Node: Heading TODO State38066
Node: Lisp Variable Set38360
Node: Heading Has Property39029
Node: Regexp Search39775
Node: Checking Tags40218
Node: Matching Headings41120
Node: Negating Conditions41717
Node: Multiple Conditions42140
Node: Consideration42822
Node: Conditional Forms45008
Node: Setting the Properties47697
Node: Extending Edna48781
Node: Naming Conventions49271
Node: Finders (1)50065
Node: Actions (1)50431
Node: Conditions (1)50896
Node: Contributing51786
Node: Bugs52652
Node: Working with EDE53009
Node: Compiling Edna54094
Node: Testing Edna54963
Node: Before Sending Changes55945
Node: Developing with Bazaar56632
Node: Documentation57373
Node: Changelog57829
Node: 10258118
Node: 10158398
Node: 1058535
Node: 10beta859049
Node: 10beta759172
Node: 10beta659466
Node: 10beta559742
Node: 10beta460129
Node: 10beta360382
Node: 10beta260821
End Tag Table
Local Variables:
coding: utf-8
End:
org-edna-1.0.2/test.mk 0000644 0001752 0001753 00000001775 13376615754 013202 0 ustar elpa elpa # Copyright (C) 2017-2018 Free Software Foundation, Inc.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# EDE only allows arbitrary code from an external makefile, so this is how we've
# got to do testing.
test: compile
@$(EMACS) \
$(EMACSFLAGS) \
$(addprefix -L ,$(LOADPATH)) \
-L "." \
-l "ert" \
-l "org-edna-tests.el" \
--eval "(setq org-edna-test-inhibit-messages t)" \
-f ert-run-tests-batch-and-exit
include Makefile
org-edna-1.0.2/org-edna.el 0000644 0001752 0001753 00000276636 13425536050 013704 0 ustar elpa elpa ;;; org-edna.el --- Extensible Dependencies 'N' Actions -*- lexical-binding: t; -*-
;; Copyright (C) 2017-2018 Free Software Foundation, Inc.
;; Author: Ian Dunn
;; Maintainer: Ian Dunn
;; Keywords: convenience, text, org
;; URL: https://savannah.nongnu.org/projects/org-edna-el/
;; Package-Requires: ((emacs "25.1") (seq "2.19") (org "9.0.5"))
;; Version: 1.0.2
;; This file is part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free Software
;; Foundation; either version 3, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
;; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
;; details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see .
;;; Commentary:
;; Edna provides an extensible means of specifying conditions which must be
;; fulfilled before a task can be completed and actions to take once it is.
;; Org Edna runs when either the BLOCKER or TRIGGER properties are set on a
;; heading, and when it is changing from a TODO state to a DONE state.
;;; History:
;;; Code:
(require 'org)
(eval-when-compile (require 'subr-x))
(require 'seq)
;; Compatibility for Emacs < 26.1
(unless (fboundp 'if-let*)
(defalias 'if-let* 'if-let))
(unless (fboundp 'when-let*)
(defalias 'when-let* 'when-let))
(defgroup org-edna nil
"Extensible Dependencies 'N' Actions"
:group 'org)
(defcustom org-edna-use-inheritance nil
"Whether Edna should use inheritance when looking for properties.
This only applies to the BLOCKER and TRIGGER properties, not any
properties used during actions or conditions."
:group 'org-edna
:type 'boolean)
(defcustom org-edna-prompt-for-archive t
"Whether Edna should prompt before archiving a target."
:group 'org-edna
:type 'boolean)
(defcustom org-edna-timestamp-format 'short
"Default timestamp format for scheduling and deadlines.
This is either 'short for short format (no time spec), or
'long (includes time spec).
When using the schedule! or deadline! actions with the ++
modifier, the current time will be used as the base time. This
leaves the potential for having no \"template\" timestamp to use
for the format. This is in contrast to the + modifier, which
uses the current timestamp's format.
The timestamp is chosen in one of three ways:
1. If the target heading already has a timestamp, that format is
used.
2. If the modifier \"thing\" is minutes or hours, long format
will always be used.
3. If the property EDNA_TS_FORMAT is set on the target heading,
it will be used. It should be either \"short\" or
\"long\" (without quotes).
4. Fallback to this variable."
:group 'org-edna
:type '(choice (const :tag "Short Format" 'short)
(const :tag "Long Format" 'long)))
;;; Form Parsing
;; 3 types of "forms" here
;;
;; 1. String form; this is what you see in a BLOCKER or TRIGGER property
;; 2. Edna sexp form; this is the intermediary form, and form used in org-edna-form
;; 3. Lisp form; a form that can be evaluated by Emacs
(defmacro org-edna--syntax-error (msg form error-pos)
"Signal an Edna syntax error.
MSG will be reported to the user and should describe the error.
FORM is the form that generated the error.
ERROR-POS is the positiong in MSG at which the error occurred."
`(signal 'invalid-read-syntax (list :msg ,msg :form ,form :error-pos ,error-pos)))
(defun org-edna--print-syntax-error (error-plist)
"Prints the syntax error from ERROR-PLIST."
(let* ((msg (plist-get error-plist :msg))
(form (plist-get error-plist :form))
(pos (plist-get error-plist :error-pos)))
(message
"Org Edna Syntax Error: %s\n%s\n%s"
msg form (concat (make-string pos ?\ ) "^"))))
(defun org-edna--transform-arg (arg)
"Transform argument ARG.
Currently, the following are handled:
- UUIDs (as determined by `org-uuidgen-p') are converted to strings
Everything else is returned as is."
(pcase arg
((and (pred symbolp) ;; Symbol
;; Name matches `org-uuidgen-p'
(let (pred org-uuidgen-p) (symbol-name arg)))
(symbol-name arg))
(_
arg)))
(defun org-edna-break-modifier (token)
"Break TOKEN into a modifier and base token.
A modifier is a single character.
Return (MODIFIER . TOKEN), even if MODIFIER is nil."
(if token
(let (modifier)
(when (string-match "^\\([!]\\)\\(.*\\)" (symbol-name token))
(setq modifier (intern (match-string 1 (symbol-name token))))
(setq token (intern (match-string 2 (symbol-name token)))))
(cons modifier token))
;; Still return something
'(nil . nil)))
(defun org-edna--function-for-key (key)
"Determine the Edna function for KEY.
KEY should be a symbol, the keyword for which to find the Edna
function.
If KEY is an invalid Edna keyword, then return nil."
(cond
;; Just return nil if it's not a symbol
((or (not key)
(not (symbolp key)))
nil)
((memq key '(consideration consider))
;; Function is ignored here, but `org-edna-describe-keyword' needs this
;; function.
(cons 'consideration 'org-edna-handle-consideration))
((string-suffix-p "!" (symbol-name key))
;; Action
(let ((func-sym (intern (format "org-edna-action/%s" key))))
(when (fboundp func-sym)
(cons 'action func-sym))))
((string-suffix-p "?" (symbol-name key))
;; Condition
(let ((func-sym (intern (format "org-edna-condition/%s" key))))
(when (fboundp func-sym)
(cons 'condition func-sym))))
(t
;; Everything else is a finder
(let ((func-sym (intern (format "org-edna-finder/%s" key))))
(when (fboundp func-sym)
(cons 'finder func-sym))))))
(defun org-edna-parse-string-form (form &optional start)
"Parse Edna string form FORM starting at position START.
Return (SEXP-FORM POS)
SEXP-FORM is the sexp form of FORM starting at START.
POS is the position in FORM where parsing ended."
(setq start (or start 0))
(pcase-let* ((`(,token . ,pos) (read-from-string form start))
(args nil))
(unless token
(org-edna--syntax-error "Invalid Token" form start))
;; Check for either end of string or an opening parenthesis
(unless (or (equal pos (length form))
(equal (string-match-p "\\s-" form pos) pos)
(equal (string-match-p "(" form pos) pos))
(org-edna--syntax-error "Invalid character in form" form pos))
;; Parse arguments if we have any
(when (equal (string-match-p "(" form pos) pos)
(pcase-let* ((`(,new-args . ,new-pos) (read-from-string form pos)))
(setq pos new-pos
args (mapcar #'org-edna--transform-arg new-args))))
;; Move across any whitespace
(when (string-match "\\s-+" form pos)
(setq pos (match-end 0)))
(list (cons token args) pos)))
(defun org-edna--convert-form (string &optional pos)
"Convert string form STRING into a flat sexp form.
POS is the position in STRING from which to start conversion.
Returns (FLAT-FORM END-POS) where
FLAT-FORM is the flat sexp form
END-POS is the position in STRING where parsing ended.
Example:
siblings todo!(TODO) => ((siblings) (todo! TODO))"
(let ((pos (or pos 0))
final-form)
(while (< pos (length string))
(pcase-let* ((`(,form ,new-pos) (org-edna-parse-string-form string pos)))
(setq final-form (append final-form (list (cons form pos))))
(setq pos new-pos)))
(cons final-form pos)))
(defun org-edna--normalize-sexp-form (form action-or-condition &optional from-string)
"Normalize flat sexp form FORM into a full edna sexp form.
ACTION-OR-CONDITION is either 'action or 'condition, indicating
which of the two types is allowed in FORM.
FROM-STRING is used internally, and is non-nil if FORM was
originally a string.
Returns (NORMALIZED-FORM REMAINING-FORM), where REMAINING-FORM is
the remainder of FORM after the current scope was parsed."
(let* ((remaining-form (copy-sequence form))
(state 'finder)
final-form
need-break)
(while (and remaining-form (not need-break))
(pcase-let* ((`(,current-form . ,error-pos) (pop remaining-form)))
(pcase (car current-form)
('if
;; Check the car of each r*-form for the expected
;; ending. If it doesn't match, throw an error.
(let (cond-form then-form else-form have-else)
(pcase-let* ((`(,temp-form ,r-form)
(org-edna--normalize-forms
remaining-form
;; Only allow conditions in cond forms
'condition
'((then))
from-string)))
(unless r-form
(org-edna--syntax-error
"Malformed if-construct; expected then terminator"
from-string error-pos))
;; Skip the 'then' construct and move forward
(setq cond-form temp-form
error-pos (cdar r-form)
remaining-form (cdr r-form)))
(pcase-let* ((`(,temp-form ,r-form)
(org-edna--normalize-forms remaining-form
action-or-condition
'((else) (endif))
from-string)))
(unless r-form
(org-edna--syntax-error
"Malformed if-construct; expected else or endif terminator"
from-string error-pos))
(setq have-else (equal (caar r-form) '(else))
then-form temp-form
error-pos (cdar r-form)
remaining-form (cdr r-form)))
(when have-else
(pcase-let* ((`(,temp-form ,r-form)
(org-edna--normalize-forms remaining-form
action-or-condition
'((endif))
from-string)))
(unless r-form
(org-edna--syntax-error "Malformed if-construct; expected endif terminator"
from-string error-pos))
(setq else-form temp-form
remaining-form (cdr r-form))))
(push `(if ,cond-form ,then-form ,else-form) final-form)))
((or 'then 'else 'endif)
(setq need-break t)
;; Push the object back on remaining-form so the if knows where we are
(setq remaining-form (cons (cons current-form error-pos) remaining-form)))
(_
;; Determine the type of the form
;; If we need to change state, return from this scope
(pcase-let* ((`(_ . ,key) (org-edna-break-modifier (car current-form)))
(`(,type . ,func) (org-edna--function-for-key key)))
(unless (and type func)
(org-edna--syntax-error "Unrecognized Form"
from-string error-pos))
(pcase type
('finder
(unless (memq state '(finder consideration))
;; We changed back to finders, so we need to start a new scope
(setq need-break t)))
('action
(unless (eq action-or-condition 'action)
(org-edna--syntax-error "Actions aren't allowed in this context"
from-string error-pos)))
('condition
(unless (eq action-or-condition 'condition)
(org-edna--syntax-error "Conditions aren't allowed in this context"
from-string error-pos))))
;; Only update state if we're not breaking. If we are, then the
;; new state doesn't matter.
(unless need-break
(setq state type))
(if need-break ;; changing state
;; Keep current-form on remaining-form so we have it for the
;; next scope, since we didn't process it here.
(setq remaining-form (cons (cons current-form error-pos) remaining-form))
(push current-form final-form)))))))
(when (and (eq state 'finder)
(eq action-or-condition 'condition))
;; Finders have to have something at the end, so we need to add that
;; something. No default actions, so this must be a blocker.
(push '(!done?) final-form))
(list (nreverse final-form) remaining-form)))
(defun org-edna--normalize-forms (form-list action-or-condition end-forms &optional from-string)
"Normalize forms in flat form list FORM-LIST until one of END-FORMS is found.
ACTION-OR-CONDITION is either 'action or 'condition, indicating
which of the two types is allowed in FORM.
FROM-STRING is used internally, and is non-nil if FORM was
originally a string.
END-FORMS is a list of forms. When one of them is found, stop parsing."
(pcase-let* ((`(,final-form ,rem-form) (org-edna--normalize-sexp-form form-list action-or-condition from-string)))
(setq final-form (list final-form))
;; Use car-safe to catch r-form = nil
(while (and rem-form (not (member (car (car-safe rem-form)) end-forms)))
(pcase-let* ((`(,new-form ,r-form)
(org-edna--normalize-sexp-form rem-form action-or-condition from-string)))
(setq final-form (append final-form (list new-form))
rem-form r-form)))
(list final-form rem-form)))
(defun org-edna--normalize-all-forms (form-list action-or-condition &optional from-string)
"Normalize all forms in flat form list FORM-LIST.
ACTION-OR-CONDITION is either 'action or 'condition, indicating
which of the two types is allowed in FORM.
FROM-STRING is used internally, and is non-nil if FORM was
originally a string."
(car-safe (org-edna--normalize-forms form-list action-or-condition nil from-string)))
(defun org-edna-string-form-to-sexp-form (string-form action-or-condition)
"Parse string form STRING-FORM into an Edna sexp form.
ACTION-OR-CONDITION is either 'action or 'condition, indicating
which of the two types is allowed in STRING-FORM."
(org-edna--normalize-all-forms
(car (org-edna--convert-form string-form))
action-or-condition
string-form))
(defun org-edna--handle-condition (func mod args targets consideration)
"Handle a condition.
FUNC is the condition function.
MOD is the modifier to pass to FUNC.
ARGS are any arguments to pass to FUNC.
TARGETS is a list of targets on which to operate.
CONSIDERATION is the consideration symbol, if any."
(when (seq-empty-p targets)
(message "Warning: Condition specified without targets"))
;; Check the condition at each target
(when-let* ((blocks
(mapcar
(lambda (entry-marker)
(org-with-point-at entry-marker
(apply func mod args)))
targets)))
;; Apply consideration
(org-edna-handle-consideration consideration blocks)))
(defun org-edna--add-targets (old-targets new-targets)
"Add targets in NEW-TARGETS to OLD-TARGETS.
Neither argument is modified."
(seq-uniq (append old-targets new-targets)))
(defun org-edna--handle-action (action targets last-entry args)
"Process ACTION on TARGETS.
LAST-ENTRY is the source entry.
ARGS is a list of arguments to pass to ACTION."
(when (seq-empty-p targets)
(message "Warning: Action specified without targets"))
(dolist (target targets)
(org-with-point-at target
(apply action last-entry args))))
(defun org-edna--expand-single-sexp-form (single-form
target-var
consideration-var
blocking-var)
"Expand sexp form SINGLE-FORM into a Lisp form.
TARGET-VAR, BLOCKING-VAR, and CONSIDERATION-VAR are symbols that
correspond to internal variables."
(pcase-let* ((`(,mkey . ,args) single-form)
(`(,mod . ,key) (org-edna-break-modifier mkey))
(`(,type . ,func) (org-edna--function-for-key key)))
(pcase type
('finder
`(setq ,target-var (org-edna--add-targets ,target-var (org-edna--handle-finder ',func ',args))))
('action
`(org-edna--handle-action ',func ,target-var (point-marker) ',args))
('condition
`(setq ,blocking-var (or ,blocking-var
(org-edna--handle-condition ',func ',mod ',args
,target-var
,consideration-var))))
('consideration
`(setq ,consideration-var ',(nth 0 args))))))
(defun org-edna--expand-sexp-form (form &optional
use-old-scope
old-target-var
old-consideration-var
old-blocking-var)
"Expand sexp form FORM into a Lisp form.
USE-OLD-SCOPE, OLD-TARGET-VAR, OLD-CONSIDERATION-VAR, and
OLD-BLOCKING-VAR are used internally."
(when form
;; We inherit the original targets, consideration, and blocking-entry when
;; we create a new scope in an if-construct.
(let* ((target-var (if use-old-scope old-target-var (cl-gentemp "targets")))
(consideration-var (if use-old-scope
old-consideration-var
(cl-gentemp "consideration")))
;; The only time we want a new blocking-var is when we are in a
;; conditional scope. Otherwise, we want the same blocking-var
;; passed through all scopes. The only time old-blocking-var won't
;; be set is if we are starting a new global scope, or we are
;; starting a conditional scope.
(blocking-var (if old-blocking-var
old-blocking-var
(cl-gentemp "blocking-entry")))
;; These won't be used if use-old-scope is non-nil
(tmp-let-binds `((,target-var ,old-target-var)
(,consideration-var ,old-consideration-var)))
;; Append blocking-var separately to avoid it attempting to let-bind nil.
(let-binds (if old-blocking-var
tmp-let-binds
(append tmp-let-binds
(list (list blocking-var nil)))))
(wrapper-form (if use-old-scope
'(progn)
`(let (,@let-binds)))))
(pcase form
(`(if ,cond ,then . ,else)
;; Don't pass the old variables into the condition form; it should be
;; evaluated on its own to avoid clobbering the old targets.
`(if (not ,(org-edna--expand-sexp-form cond))
,(org-edna--expand-sexp-form
then
t
old-target-var old-consideration-var old-blocking-var)
,(when else
(org-edna--expand-sexp-form
;; else is wrapped in a list, so take the first argument
(car else)
t
old-target-var old-consideration-var old-blocking-var))))
((pred (lambda (arg) (symbolp (car arg))))
(org-edna--expand-single-sexp-form
form old-target-var old-consideration-var old-blocking-var))
(_
;; List of forms
;; Only use new variables if we're asked to
`(,@wrapper-form
,@(mapcar
(lambda (f) (org-edna--expand-sexp-form
f nil target-var consideration-var blocking-var))
form)))))))
(defun org-edna-eval-sexp-form (sexp-form)
"Evaluate Edna sexp form SEXP-FORM."
(eval
(org-edna--expand-sexp-form sexp-form)))
(defun org-edna-process-form (string-form action-or-condition)
"Process STRING-FORM.
ACTION-OR-CONDITION is either 'action or 'condition, indicating
which of the two types is allowed in STRING-FORM."
(org-edna-eval-sexp-form
(org-edna-string-form-to-sexp-form string-form action-or-condition)))
;;; Cache
;; Cache works because the returned values of finders are all markers. Markers
;; will automatically update themselves when a buffer is edited.
;; We use a timeout for cache because it's expected that the Org files
;; themselves will change. Thus, there's no assured way to determine if we need
;; to update the cache without actually running again. Therefore, we assume
;; most operations that the user wants to expedite will be performed in bulk.
(cl-defstruct org-edna--finder-input
func-sym args)
(cl-defstruct org-edna--finder-cache-entry
input results last-run-time)
(defvar org-edna--finder-cache (make-hash-table :test 'equal))
(defcustom org-edna-finder-use-cache nil
"Whether to use cache for improved performance with finders.
When cache is used for a finder, each finder call will store its
results for up to `org-edna-finder-cache-timeout' seconds. The
results and input are both stored, so the same form for a given
finder will yield the results of the previous call.
If enough time has passed since the results in cache for a
specific form were generated, the results will be regenerated and
stored in cache.
Minor changes to an Org file, such as setting properties or
adding unrelated headings, will be taken into account."
:group 'org-edna
:type 'boolean)
(defcustom org-edna-finder-cache-timeout 300
"Maximum age to keep entries in cache, in seconds."
:group 'org-edna
:type 'number)
(defvar org-edna-finder-cache-enabled-finders
'(org-edna-finder/match
org-edna-finder/ids
org-edna-finder/olp
org-edna-finder/file
org-edna-finder/org-file)
"List of finders for which cache is enabled.
Only edit this list if you've added custom finders. Many
finders, specifically relative finders, rely on the context in
which they're called. For these finders, cache will not work
properly.
The default state of this list contains the built-in finders for
which context is irrelevant.
Each entry is the function symbol for the finder.")
(defun org-edna--add-to-finder-cache (func-sym args)
(let* ((results (apply func-sym args))
(input (make-org-edna--finder-input :func-sym func-sym
:args args))
(entry (make-org-edna--finder-cache-entry :input input
:results results
:last-run-time (current-time))))
(puthash input entry org-edna--finder-cache)
;; Returning the results here passes them to the calling function. It's the
;; only part of the entry we care about here.
results))
(defun org-edna--finder-cache-timeout (_func-sym)
;; In the future, we may want to support configurable timeouts on a per-finder
;; basis.
org-edna-finder-cache-timeout)
(defun org-edna--get-cache-entry (func-sym args)
"Find a valid entry in the cache.
If none exists, return nil. An entry is invalid for any of the
following reasons:
- It doesn't exist
- It has timed out
- It contains an invalid marker"
(let* ((input (make-org-edna--finder-input :func-sym func-sym
:args args))
(entry (gethash input org-edna--finder-cache)))
(cond
;; If we don't have an entry, rerun and make a new one.
((not entry) nil)
;; If we do have an entry, but it's timed out, then create a new one.
((>= (float-time (time-subtract (current-time)
(org-edna--finder-cache-entry-last-run-time entry)))
(org-edna--finder-cache-timeout func-sym))
nil)
;; If any element of the results is an invalid marker, then rerun.
((seq-find (lambda (x) (not (markerp x))) (org-edna--finder-cache-entry-results entry) nil)
nil)
;; We have an entry created within the allowed interval.
(t entry))))
(defun org-edna--cache-is-enabled-for-finder (func-sym)
(memq func-sym org-edna-finder-cache-enabled-finders))
(defun org-edna--handle-finder (func-sym args)
(if (or (not org-edna-finder-use-cache)
(not (org-edna--cache-is-enabled-for-finder func-sym)))
;; Not using cache, so use the function directly.
(apply func-sym args)
(let* ((entry (org-edna--get-cache-entry func-sym args)))
(if entry
(org-edna--finder-cache-entry-results entry)
;; Adds the entry to the cache, and returns the results.
(org-edna--add-to-finder-cache func-sym args)))))
(defun org-edna-reset-cache ()
"Reset the finder cache.
Use this only if there's a problem with the cache.
When an Org mode buffer is reverted, the cache will be made
useless for that buffer. Therefore, it's a good idea to call
this after reverting Org mode buffers."
(interactive)
(setq org-edna--finder-cache (make-hash-table :test 'equal)))
;;; Interactive Functions
(defmacro org-edna-run (change-plist &rest body)
"Run a TODO state change.
The state information is held in CHANGE-PLIST. If the TODO state
is changing from a TODO state to a DONE state, run BODY."
(declare (indent 1))
`(let* ((pos (plist-get ,change-plist :position))
(type (plist-get ,change-plist :type))
(from (plist-get ,change-plist :from))
(to (plist-get ,change-plist :to)))
(if (and
;; We are only handling todo-state-change
(eq type 'todo-state-change)
;; And only from a TODO state to a DONE state
(member from (cons 'todo org-not-done-keywords))
(member to (cons 'done org-done-keywords)))
(condition-case-unless-debug err
,@body
(error
(if (eq (car err) 'invalid-read-syntax)
(org-edna--print-syntax-error (cdr err))
(message "Edna Error at heading %s: %s" (org-get-heading t t t) (error-message-string err)))
(setq org-block-entry-blocking (org-get-heading))
;; Block
nil))
;; Return t for the blocker to let the calling function know that there
;; is no block here.
t)))
(defun org-edna-trigger-function (change-plist)
"Trigger function work-horse.
See `org-edna-run' for CHANGE-PLIST explanation.
This shouldn't be run from outside of `org-trigger-hook'."
(org-edna-run change-plist
(when-let* ((form (org-entry-get pos "TRIGGER" org-edna-use-inheritance)))
(org-edna-process-form form 'action))))
(defun org-edna-blocker-function (change-plist)
"Blocker function work-horse.
See `org-edna-run' for CHANGE-PLIST explanation.
This shouldn't be run from outside of `org-blocker-hook'."
(org-edna-run change-plist
(if-let* ((form (org-entry-get pos "BLOCKER" org-edna-use-inheritance)))
;; Return nil if there is no blocking entry
(not (setq org-block-entry-blocking (org-edna-process-form form 'condition)))
t)))
;;;###autoload
(defun org-edna-load ()
"Setup the hooks necessary for Org Edna to run.
This means adding to `org-trigger-hook' and `org-blocker-hook'."
(interactive)
(add-hook 'org-trigger-hook 'org-edna-trigger-function)
(add-hook 'org-blocker-hook 'org-edna-blocker-function))
;;;###autoload
(defun org-edna-unload ()
"Unload Org Edna.
Remove Edna's workers from `org-trigger-hook' and
`org-blocker-hook'."
(interactive)
(remove-hook 'org-trigger-hook 'org-edna-trigger-function)
(remove-hook 'org-blocker-hook 'org-edna-blocker-function))
;;; Finders
;; Tag Finder
(defun org-edna-finder/match (match-spec &optional scope skip)
"Find entries using Org matching.
Edna Syntax: match(\"MATCH-SPEC\" SCOPE SKIP)
MATCH-SPEC may be any valid match string; it is passed straight
into `org-map-entries'.
SCOPE and SKIP are their counterparts in `org-map-entries'.
SCOPE defaults to agenda, and SKIP defaults to nil. Because of
the different defaults in SCOPE, the symbol 'buffer may also be
used. This indicates that scope should be the current buffer,
honoring any restriction (the equivalent of the nil SCOPE in
`org-map-entries'.)
* TODO Test
:PROPERTIES:
:BLOCKER: match(\"test&mine\" agenda)
:END:
\"Test\" will block until all entries tagged \"test\" and
\"mine\" in the agenda files are marked DONE."
;; Our default is agenda...
(setq scope (or scope 'agenda))
;; ...but theirs is the buffer
(when (eq scope 'buffer) (setq scope nil))
(org-map-entries
;; Find all entries in the agenda files that match the given tag.
(lambda nil (point-marker))
match-spec scope skip))
;; ID finder
(defun org-edna-finder/ids (&rest ids)
"Find a list of headings with given IDS.
Edna Syntax: ids(ID1 ID2 ...)
Each ID is a UUID as understood by `org-id-find'.
Note that in the edna syntax, the IDs don't need to be quoted."
(mapcar (lambda (id) (org-id-find id 'marker)) ids))
(defun org-edna-finder/self ()
"Finder for the current heading.
Edna Syntax: self"
(list (point-marker)))
(defun org-edna-first-sibling ()
"Return a marker to the first child of the current level."
(org-with-wide-buffer
(org-up-heading-safe)
(org-goto-first-child)
(point-marker)))
(defun org-edna-last-sibling ()
"Return a marker to the first child of the current level."
;; Unfortunately, we have to iterate through every heading on this level to
;; find the first one.
(org-with-wide-buffer
(while (org-goto-sibling)
;; Do nothing, just keep going down
)
(point-marker)))
(defun org-edna-goto-sibling (&optional previous wrap)
"Move to the next sibling on the same level as the current heading.
If PREVIOUS is non-nil, go to the previous sibling.
f WRAP is non-nil, wrap around when the beginning (or end) is
reached."
(let ((next (save-excursion
(if previous (org-get-last-sibling) (org-get-next-sibling)))))
(cond
;; We have a sibling, so go to it and return non-nil
(next (goto-char next))
;; We have no sibling, and we're not wrapping, so return nil
((not wrap) nil)
(t
;; Go to the first child if going forward, or the last if going backward,
;; and return non-nil.
(goto-char
(if previous
(org-edna-last-sibling)
(org-edna-first-sibling)))
t))))
(defun org-edna-self-marker ()
"Return a marker to the current heading."
(org-with-wide-buffer
(and (ignore-errors (org-back-to-heading t) (point-marker)))))
(defun org-edna-collect-current-level (start backward wrap include-point)
"Collect the headings on the current level.
START is a point or marker from which to start collection.
BACKWARD means go backward through the level instead of forward.
If WRAP is non-nil, wrap around when the end of the current level
is reached.
If INCLUDE-POINT is non-nil, include the current point."
(org-with-wide-buffer
(let ((markers))
(goto-char start)
;; Handle including point
(when include-point
(push (point-marker) markers))
(while (and (org-edna-goto-sibling backward wrap)
(not (equal (point-marker) start)))
(push (point-marker) markers))
(nreverse markers))))
(defun org-edna-collect-ancestors (&optional with-self)
"Collect the ancestors of the current subtree.
If WITH-SELF is non-nil, include the current subtree in the list
of ancestors.
Return a list of markers for the ancestors."
(let ((markers))
(when with-self
(push (point-marker) markers))
(org-with-wide-buffer
(while (org-up-heading-safe)
(push (point-marker) markers)))
(nreverse markers)))
(defun org-edna-collect-descendants (&optional with-self)
"Collect the descendants of the current subtree.
If WITH-SELF is non-nil, include the current subtree in the list
of descendants.
Return a list of markers for the descendants."
(let ((targets
(org-with-wide-buffer
(org-map-entries
(lambda nil (point-marker))
nil 'tree))))
;; Remove the first one (self) if we didn't want self
(unless with-self
(pop targets))
targets))
(defun org-edna-entry-has-tags-p (&rest tags)
"Return non-nil if the current entry has any tags in TAGS."
(when-let* ((entry-tags (org-get-tags nil t)))
(seq-intersection tags entry-tags)))
(defun org-edna--get-timestamp-time (pom &optional inherit)
"Get the timestamp time as a time tuple, of a format suitable
for calling org-schedule with, or if there is no timestamp,
returns nil."
(let ((time (org-entry-get pom "TIMESTAMP" inherit)))
(when time
(apply 'encode-time (org-parse-time-string time)))))
(defun org-edna-finder/relatives (&rest options)
"Find some relative of the current heading.
Edna Syntax: relatives(OPTION OPTION...)
Edna Syntax: chain-find(OPTION OPTION...)
Identical to the chain argument in org-depend, relatives selects
its single target using the following method:
1. Creates a list of possible targets
2. Filters the targets from Step 1
3. Sorts the targets from Step 2
One option from each of the following three categories may be
used; if more than one is specified, the last will be used.
Filtering is the exception to this; each filter argument adds to
the current filter. Apart from that, argument order is
irrelevant.
The chain-find finder is also provided for backwards
compatibility, and for similarity to org-depend.
All arguments are symbols, unless noted otherwise.
*Selection*
- from-top: Select siblings of the current heading, starting at the top
- from-bottom: As above, but from the bottom
- from-current: Selects siblings, starting from the heading (wraps)
- no-wrap: As above, but without wrapping
- forward-no-wrap: Find entries on the same level, going forward
- forward-wrap: As above, but wrap when the end is reached
- backward-no-wrap: Find entries on the same level, going backward
- backward-wrap: As above, but wrap when the start is reached
- walk-up: Walk up the tree, excluding self
- walk-up-with-self: As above, but including self
- walk-down: Recursively walk down the tree, excluding self
- walk-down-with-self: As above, but including self
- step-down: Collect headings from one level down
*Filtering*
- todo-only: Select only targets with TODO state set that isn't a DONE state
- todo-and-done-only: Select all targets with a TODO state set
- no-comments: Skip commented headings
- no-archive: Skip archived headings
- NUMBER: Only use that many headings, starting from the first one
If passed 0, use all headings
If <0, omit that many headings from the end
- \"+tag\": Only select headings with given tag
- \"-tag\": Only select headings without tag
- \"REGEX\": select headings whose titles match REGEX
*Sorting*
- no-sort: Remove other sorting in affect
- reverse-sort: Reverse other sorts (stacks with other sort methods)
- random-sort: Sort in a random order
- priority-up: Sort by priority, highest first
- priority-down: Same, but lowest first
- effort-up: Sort by effort, highest first
- effort-down: Sort by effort, lowest first
- scheduled-up: Scheduled time, farthest first
- scheduled-down: Scheduled time, closest first
- deadline-up: Deadline time, farthest first
- deadline-down: Deadline time, closest first
- timestamp-up: Timestamp time, farthest first
- timestamp-down: Timestamp time, closest first"
(let (targets
sortfun
reverse-sort
(idx 0) ;; By default, use all entries
filterfuns ;; No filtering by default
;; From org-depend.el:
;; (and (not todo-and-done-only)
;; (member (second item) org-done-keywords))
)
(dolist (opt options)
(pcase opt
('from-top
(setq targets (org-edna-collect-current-level (org-edna-first-sibling) nil nil t)))
('from-bottom
(setq targets (org-edna-collect-current-level (org-edna-last-sibling) t nil t)))
((or 'from-current 'forward-wrap)
(setq targets (org-edna-collect-current-level (org-edna-self-marker) nil t nil)))
((or 'no-wrap 'forward-no-wrap)
(setq targets (org-edna-collect-current-level (org-edna-self-marker) nil nil nil)))
('backward-no-wrap
(setq targets (org-edna-collect-current-level (org-edna-self-marker) t nil nil)))
('backward-wrap
(setq targets (org-edna-collect-current-level (org-edna-self-marker) t t nil)))
('walk-up
(setq targets (org-edna-collect-ancestors nil)))
('walk-up-with-self
(setq targets (org-edna-collect-ancestors t)))
('walk-down
(setq targets (org-edna-collect-descendants nil)))
('walk-down-with-self
(setq targets (org-edna-collect-descendants t)))
('step-down
(setq targets
(org-with-wide-buffer
(org-goto-first-child)
(org-edna-collect-current-level (org-edna-self-marker) nil nil t))))
('todo-only
;; Remove any entry without a TODO keyword, or with a DONE keyword
(cl-pushnew
(lambda (target)
(let ((kwd (org-entry-get target "TODO")))
(or (not kwd)
(member kwd org-done-keywords))))
filterfuns
:test 'equal))
('todo-and-done-only
;; Remove any entry without a TODO keyword
(cl-pushnew
(lambda (target)
(not (org-entry-get target "TODO")))
filterfuns :test 'equal))
((pred numberp)
(setq idx opt))
((and (pred stringp)
(pred (lambda (opt) (string-match-p "^\\+" opt))))
(cl-pushnew
(lambda (target)
;; This is a function that will return non-nil if the entry should
;; be removed, so remove those entries that don't have the tag
(org-with-point-at target
(not (org-edna-entry-has-tags-p (string-remove-prefix "+" opt)))))
filterfuns :test 'equal))
((and (pred stringp)
(pred (lambda (opt) (string-match-p "^\\-" opt))))
(cl-pushnew
(lambda (target)
;; This is a function that will return non-nil if the entry should
;; be removed, so remove those entries that DO have the tag
(org-with-point-at target
(org-edna-entry-has-tags-p (string-remove-prefix "-" opt))))
filterfuns :test 'equal))
((pred stringp)
(cl-pushnew
(lambda (target)
;; Return non-nil if entry doesn't match the regular expression, so
;; it will be removed.
(not (string-match-p opt
(org-with-point-at target
(org-get-heading t t t t)))))
filterfuns :test 'equal))
('no-comment
(cl-pushnew
(lambda (target)
(org-with-point-at target
(org-in-commented-heading-p)))
filterfuns :test 'equal))
('no-archive
(cl-pushnew
(lambda (target)
(org-with-point-at target
(org-edna-entry-has-tags-p org-archive-tag)))
filterfuns :test 'equal))
('no-sort
(setq sortfun nil
reverse-sort nil))
('random-sort
(setq sortfun
(lambda (_rhs _lhs)
(let ((l (random 100))
(r (random 100)))
(< l r)))))
('reverse-sort
(setq reverse-sort t))
('priority-up
;; A is highest priority, but assigned the lowest value, so we need to
;; reverse the sort here.
(setq sortfun
(lambda (lhs rhs)
(let ((priority-lhs (org-entry-get lhs "PRIORITY"))
(priority-rhs (org-entry-get rhs "PRIORITY")))
(string-lessp priority-lhs priority-rhs)))))
('priority-down
(setq sortfun
(lambda (lhs rhs)
(let ((priority-lhs (org-entry-get lhs "PRIORITY"))
(priority-rhs (org-entry-get rhs "PRIORITY")))
(not (string-lessp priority-lhs priority-rhs))))))
('effort-up
(setq sortfun
(lambda (lhs rhs)
(let ((effort-lhs (org-duration-to-minutes (org-entry-get lhs "EFFORT")))
(effort-rhs (org-duration-to-minutes (org-entry-get rhs "EFFORT"))))
(not (< effort-lhs effort-rhs))))))
('effort-down
(setq sortfun
(lambda (lhs rhs)
(let ((effort-lhs (org-duration-to-minutes (org-entry-get lhs "EFFORT")))
(effort-rhs (org-duration-to-minutes (org-entry-get rhs "EFFORT"))))
(< effort-lhs effort-rhs)))))
('scheduled-up
(setq sortfun
(lambda (lhs rhs)
(let ((time-lhs (org-get-scheduled-time lhs))
(time-rhs (org-get-scheduled-time rhs)))
(not (time-less-p time-lhs time-rhs))))))
('scheduled-down
(setq sortfun
(lambda (lhs rhs)
(let ((time-lhs (org-get-scheduled-time lhs))
(time-rhs (org-get-scheduled-time rhs)))
(time-less-p time-lhs time-rhs)))))
('deadline-up
(setq sortfun
(lambda (lhs rhs)
(let ((time-lhs (org-get-deadline-time lhs))
(time-rhs (org-get-deadline-time rhs)))
(not (time-less-p time-lhs time-rhs))))))
('deadline-down
(setq sortfun
(lambda (lhs rhs)
(let ((time-lhs (org-get-deadline-time lhs))
(time-rhs (org-get-deadline-time rhs)))
(time-less-p time-lhs time-rhs)))))
('timestamp-up
(setq sortfun
(lambda (lhs rhs)
(let ((time-lhs (org-edna--get-timestamp-time lhs))
(time-rhs (org-edna--get-timestamp-time rhs)))
(not (time-less-p time-lhs time-rhs))))))
('timestamp-down
(setq sortfun
(lambda (lhs rhs)
(let ((time-lhs (org-edna--get-timestamp-time lhs))
(time-rhs (org-edna--get-timestamp-time rhs)))
(time-less-p time-lhs time-rhs)))))))
(setq filterfuns (nreverse filterfuns))
(when (and targets sortfun)
(setq targets (seq-sort sortfun targets)))
(dolist (filterfun filterfuns)
(setq targets (seq-remove filterfun targets)))
(when reverse-sort
(setq targets (nreverse targets)))
(when (and targets (/= idx 0))
(if (> idx (seq-length targets))
(message "Edna relatives finder got index %s out of bounds of target size; ignoring" idx)
(setq targets (seq-subseq targets 0 idx))))
targets))
(defalias 'org-edna-finder/chain-find 'org-edna-finder/relatives)
(defun org-edna-finder/siblings (&rest options)
"Finder for all siblings of the source heading.
Edna Syntax: siblings(OPTIONS...)
Siblings are returned in order, starting from the first heading.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 'from-top options))
(defun org-edna-finder/rest-of-siblings (&rest options)
"Finder for the siblings after the source heading.
Edna Syntax: rest-of-siblings(OPTIONS...)
Siblings are returned in order, starting from the first heading
after the source heading.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 'forward-no-wrap options))
(defun org-edna-finder/rest-of-siblings-wrap (&rest options)
"Finder for all siblings of the source heading.
Edna Syntax: rest-of-siblings-wrap(OPTIONS...)
Siblings are returned in order, starting from the first heading
after the source heading and wrapping when it reaches the end.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 'forward-wrap options))
(defalias 'org-edna-finder/siblings-wrap 'org-edna-finder/rest-of-siblings-wrap)
(defun org-edna-finder/next-sibling (&rest options)
"Finder for the next sibling after the source heading.
Edna Syntax: next-sibling(OPTIONS...)
If the source heading is the last of its siblings, no target is
returned.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 1 'forward-no-wrap options))
(defun org-edna-finder/next-sibling-wrap (&rest options)
"Finder for the next sibling after the source heading.
Edna Syntax: next-sibling-wrap(OPTIONS...)
If the source heading is the last of its siblings, its first
sibling is returned.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 1 'forward-wrap options))
(defun org-edna-finder/previous-sibling (&rest options)
"Finder for the first sibling before the source heading.
Edna Syntax: previous-sibling(OPTIONS...)
If the source heading is the first of its siblings, no target is
returned.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 1 'backward-no-wrap options))
(defun org-edna-finder/previous-sibling-wrap (&rest options)
"Finder for the first sibling before the source heading.
Edna Syntax: previous-sibling-wrap(OPTIONS...)
If the source heading is the first of its siblings, no target is
returned.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 1 'backward-wrap options))
(defun org-edna-finder/first-child (&rest options)
"Return the first child of the source heading.
Edna Syntax: first-child(OPTIONS...)
If the source heading has no children, no target is returned.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 1 'step-down options))
(defun org-edna-finder/children (&rest options)
"Finder for the immediate children of the source heading.
Edna Syntax: children(OPTIONS...)
If the source has no children, no target is returned.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 'step-down options))
(defun org-edna-finder/parent (&rest options)
"Finder for the parent of the source heading.
Edna Syntax: parent(OPTIONS...)
If the source heading is a top-level heading, no target is
returned.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 1 'walk-up options))
(defun org-edna-finder/descendants (&rest options)
"Finder for all descendants of the source heading.
Edna Syntax: descendants(OPTIONS...)
This is ALL descendants of the source heading, across all
levels. This also includes the source heading.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 'walk-down options))
(defun org-edna-finder/ancestors (&rest options)
"Finder for the ancestors of the source heading.
Edna Syntax: ancestors(OPTIONS...)
Example:
* TODO Heading 1
** TODO Heading 2
*** TODO Heading 3
**** TODO Heading 4
***** TODO Heading 5
:PROPERTIES:
:BLOCKER: ancestors
:END:
In the above example, Heading 5 will be blocked until Heading 1,
Heading 3, and Heading 4 are marked DONE, while Heading 2 is
ignored.
See `org-edna-finder/relatives' for the OPTIONS argument."
(apply 'org-edna-finder/relatives 'walk-up options))
(defun org-edna-finder/olp (file olp)
"Finder for heading by its outline path.
Edna Syntax: olp(\"FILE\" \"OLP\")
Finds the heading given by OLP in FILE. Both arguments are
strings. OLP is an outline path. Example:
* TODO Test
:PROPERTIES:
:BLOCKER: olp(\"test.org\" \"path/to/heading\")
:END:
Test will block if the heading \"path/to/heading\" in
\"test.org\" is not DONE."
(let ((marker (org-find-olp (cons file (split-string-and-unquote olp "/")))))
(when (markerp marker)
(list marker))))
;; TODO: Clean up the buffer when it's finished
(defun org-edna-finder/file (file)
"Finder for a file by its name.
Edna Syntax: file(\"FILE\")
FILE is the full path to the desired file. The returned target
will be the minimum point in the file.
* TODO Test
:PROPERTIES:
:BLOCKER: file(\"~/myfile.org\") headings?
:END:
Here, \"Test\" will block until myfile.org is clear of headings.
Note that this does not give a valid heading, so any conditions
or actions that require will throw an error. Consult the
documentation for individual actions or conditions to determine
which ones will and won't work."
;; If there isn't a buffer visiting file, then there's no point in having a
;; marker to the start of the file, so use `find-file-noselect'.
(with-current-buffer (find-file-noselect file)
(list (point-min-marker))))
(defun org-edna-finder/org-file (file)
"Finder for FILE in `org-directory'.
Edna Syntax: org-file(\"FILE\")
FILE is the relative path of a file in `org-directory'. Nested
files are allowed, such as \"my-directory/my-file.org\". The
returned target is the minimum point of FILE.
* TODO Test
:PROPERTIES:
:BLOCKER: org-file(\"test.org\")
:END:
Note that the file still requires an extension; the \"org\" here
just means to look in `org-directory', not necessarily an
`org-mode' file.
Note that this does not give a valid heading, so any conditions
or actions that require will throw an error. Consult the
documentation for individual actions or conditions to determine
which ones will and won't work."
(with-current-buffer (find-file-noselect (expand-file-name file org-directory))
(list (point-min-marker))))
;;; Actions
;; Set TODO state
(defun org-edna-action/todo! (_last-entry new-state)
"Action to set a target heading's TODO state to NEW-STATE.
Edna Syntax: todo!(NEW-STATE)
Edna Syntax: todo!(\"NEW-STATE\")
NEW-STATE may either be a symbol or a string. If it is a symbol,
the symbol name is used for the new state. Otherwise, it is a
string for the new state, or \"\" to remove the state."
(org-todo (if (stringp new-state) new-state (symbol-name new-state))))
;; Set planning info
(defun org-edna--mod-timestamp (time-stamp n what)
"Modify the timestamp TIME-STAMP by N WHATs.
N is an integer. WHAT can be `day', `month', `year', `minute',
`second'."
(with-temp-buffer
(insert time-stamp)
(goto-char (point-min))
(org-timestamp-change n what)
(buffer-string)))
(defun org-edna--property-for-planning-type (type)
(pcase type
('scheduled "SCHEDULED")
('deadline "DEADLINE")
('timestamp "TIMESTAMP")
(_ "")))
(defun org-edna--get-planning-info (what)
"Get the planning info for WHAT.
WHAT is one of 'scheduled, 'deadline, or 'timestamp."
(org-entry-get nil (org-edna--property-for-planning-type what)))
;; Silence the byte-compiler
(defvar parse-time-weekdays)
(defvar parse-time-months)
(defun org-edna--read-date-get-relative (s today default)
"Like `org-read-date-get-relative' but with a few additions.
S is a string with the form [+|-|++|--][N]THING.
THING may be any of the following:
- A weekday (WEEKDAY), in which case the number of days from
either TODAY or DEFAULT to the next WEEKDAY will be computed.
If N is given, jump forward that many occurrences of WEEKDAY
- The string \"weekday\" or \"wkdy\", in which jump forward X
days to land on a weekday. If a weekend is found instead, move
in the direction given (+/-) until a weekday is found.
S may also end with [+|-][DAY]. DAY may be either a weekday
string, such as Monday, Tue, or Friday, or the strings
\"weekday\", \"wkdy\", \"weekend\", or \"wknd\". The former
indicates that the time should land on the given day of the week,
while the latter group indicates that the time should land on
that type, either a weekday or a weekend. The [+|-] in this
string indicates that the time should be incremented or
decremented to find the target day.
Return shift list (N what def-flag) to get to the desired date
WHAT is \"M\", \"h\", \"d\", \"w\", \"m\", or \"y\" for minute, hour, day, week, month, year.
N is the number of WHATs to shift.
DEF-FLAG is t when a double ++ or -- indicates shift relative to
the DEFAULT date rather than TODAY.
Examples:
\"+1d +wkdy\" finds the number of days to move ahead in order to
find a weekday. This is the same as \"+1wkdy\", and returns (N \"d\" nil).
\"+5d -wkdy\" means move forward 5 days, then backward until a
weekday is found. Returns (N \"d\" nil).
\"+1m +wknd\" means move forward one month, then forward until a
weekend is found. Returns (N \"d\" nil), since day precision is
required."
(require 'parse-time)
(let* ((case-fold-search t) ;; ignore case when matching, so we get any
;; capitalization of weekday names
(weekdays (mapcar 'car parse-time-weekdays))
;; type-strings maps the type of thing to the index in decoded time
;; (see `decode-time')
(type-strings '(("M" . 1)
("h" . 2)
("d" . 3)
("w" . 3)
("m" . 4)
("y" . 5)))
(regexp (rx-to-string
`(and string-start
;; Match 1: [+-]
(submatch (repeat 0 2 (in ?+ ?-)))
;; Match 2: Digits
(submatch (zero-or-more digit))
;; Match 3: type string (weekday, unit)
(submatch (or (any ,@(mapcar 'car type-strings))
"weekday" "wkdy"
,@weekdays)
word-end)
;; Match 4 (optional): Landing specifier
(zero-or-one
(submatch (and (one-or-more " ")
(submatch (zero-or-one (in ?+ ?-)))
(submatch (or "weekday" "wkdy"
"weekend" "wknd"
,@weekdays)
word-end))))
string-end))))
(when (string-match regexp s)
(let* ((dir (if (> (match-end 1) (match-beginning 1))
(string-to-char (substring (match-string 1 s) -1))
?+))
(rel (and (match-end 1) (= 2 (- (match-end 1) (match-beginning 1)))))
(n (if (match-end 2) (string-to-number (match-string 2 s)) 1))
(what (if (match-end 3) (match-string 3 s) "d"))
(wday1 (cdr (assoc (downcase what) parse-time-weekdays)))
(date (if rel default today))
(wday (nth 6 (decode-time date)))
;; Are we worrying about where we land?
(have-landing (match-end 4))
(landing-direction (string-to-char
(if (and have-landing (match-end 5))
(match-string 5 s)
"+")))
(landing-type (when have-landing (match-string 6 s)))
delta ret)
(setq
ret
(pcase what
;; Shorthand for +Nd +wkdy or -Nd -wkdy
((or "weekday" "wkdy")
;; Determine where we land after N days
(let* ((del (* n (if (= dir ?-) -1 1)))
(end-day (mod (+ del wday) 7)))
(while (member end-day calendar-weekend-days)
(let ((d (if (= dir ?-) -1 1)))
(cl-incf del d)
(setq end-day (mod (+ end-day d) 7))))
(list del "d" rel)))
((pred (lambda (arg) (member arg (mapcar 'car type-strings))))
(list (* n (if (= dir ?-) -1 1)) what rel))
((pred (lambda (arg) (member arg weekdays)))
(setq delta (mod (+ 7 (- wday1 wday)) 7))
(when (= delta 0) (setq delta 7))
(when (= dir ?-)
(setq delta (- delta 7))
(when (= delta 0) (setq delta -7)))
(when (> n 1) (setq delta (+ delta (* (1- n) (if (= dir ?-) -7 7)))))
(list delta "d" rel))))
(if (or (not have-landing)
(member what '("M" "h"))) ;; Don't change landing for minutes or hours
ret ;; Don't worry about landing, just return
(pcase-let* ((`(,del ,what _) ret)
(mod-index (cdr (assoc what type-strings)))
;; Increment the appropriate entry in the original decoded time
(raw-landing-time
(let ((tmp (copy-sequence (decode-time date))))
(cl-incf (seq-elt tmp mod-index)
;; We increment the days by 7 when we have weeks
(if (string-equal what "w") (* 7 del) del))
tmp))
(encoded-landing-time (apply 'encode-time raw-landing-time))
;; Get the initial time difference in days, rounding down
;; (it should be something like 3.0, so it won't matter)
(time-diff (truncate
(/ (float-time (time-subtract encoded-landing-time
date))
86400))) ;; seconds in a day
;; Decoded landing time
(landing-time (decode-time encoded-landing-time))
;; Numeric Landing direction
(l-dir (if (= landing-direction ?-) -1 1))
;; Current numeric day of the week on which we end
(end-day (nth 6 landing-time))
;; Numeric days of the week on which we are allowed to land
(allowed-targets
(pcase landing-type
((or "weekday" "wkdy")
(seq-difference (number-sequence 0 6) calendar-weekend-days))
((or "weekend" "wknd")
calendar-weekend-days)
((pred (lambda (arg) (member arg weekdays)))
(list (cdr (assoc (downcase landing-type) parse-time-weekdays)))))))
;; While we aren't looking at a valid day, move one day in the l-dir
;; direction.
(while (not (member end-day allowed-targets))
(cl-incf time-diff l-dir)
(setq end-day (mod (+ end-day l-dir) 7)))
(list time-diff "d" rel)))))))
(defun org-edna--float-time (arg this-time default)
"Read a float time string from ARG.
A float time argument string is as follows:
float [+|-|++|--]?N DAYNAME[ MONTH[ DAY]]
N is an integer
DAYNAME is either an integer day of the week, or a weekday string
MONTH may be a month string or an integer. Use 0 for the
following or previous month.
DAY is an optional integer. If not given, it will be 1 (for
forward) or the last day of MONTH (backward).
Time is computed relative to either THIS-TIME (+/-) or
DEFAULT (++/--)."
(require 'parse-time)
(let* ((case-fold-search t)
(weekdays (mapcar 'car parse-time-weekdays))
(month-names (mapcar 'car parse-time-months))
(regexp (rx-to-string
`(and string-start
"float "
;; First argument, N
(submatch (repeat 0 2 (in ?+ ?-)))
(submatch word-start (one-or-more digit) word-end)
" "
;; Second argument, weekday digit or string
(submatch word-start
(or (in (?0 . ?6)) ;; Weekday digit
,@weekdays)
word-end)
;; Third argument, month digit or string
(zero-or-one
" " (submatch word-start
(or (repeat 1 2 digit)
,@month-names)
word-end)
;; Fourth argument, day in month
(zero-or-one
" "
(submatch word-start
(repeat 1 2 digit)
word-end)))))))
(when (string-match regexp arg)
(pcase-let* ((inc (match-string 1 arg))
(dir (if (not (string-empty-p inc)) ;; non-empty string
(string-to-char (substring inc -1))
?+))
(rel (= (length inc) 2))
(numeric-dir (if (= dir ?+) 1 -1))
(nth (* (string-to-number (match-string 2 arg)) numeric-dir))
(dayname (let* ((tmp (match-string 3 arg))
(day (cdr (assoc (downcase tmp) parse-time-weekdays))))
(or day (string-to-number tmp))))
(month (if-let* ((tmp (match-string 4 arg)))
(or (cdr (assoc (downcase tmp) parse-time-months))
(string-to-number tmp))
0))
(day (if (match-end 5) (string-to-number (match-string 5 arg)) 0))
(ts (if rel default this-time))
(`(_ _ _ ,dec-day ,dec-month ,dec-year _ _ _) (decode-time ts))
;; If month isn't given, use the 1st of the following (or previous) month
;; If month is given, use the 1st (or day, if given) of that
;; following month
(month-given (not (= month 0)))
;; If day isn't provided, pass nil to
;; `calendar-nth-named-absday' so it can handle it.
(act-day (if (not (= day 0)) day nil))
(`(,act-month ,act-year)
(if (not month-given)
;; Month wasn't given, so start at the following or previous month.
(list (+ dec-month (if (= dir ?+) 1 -1)) dec-year)
;; Month was given, so adjust the year accordingly
(cond
;; If month is after dec-month and we're incrementing,
;; keep year
((and (> month dec-month) (= dir ?+))
(list month dec-year))
;; If month is before or the same as dec-month, and we're
;; incrementing, increment year.
((and (<= month dec-month) (= dir ?+))
(list month (1+ dec-year)))
;; We're moving backwards, but month is after, so
;; decrement year.
((and (>= month dec-month) (= dir ?-))
(list month (1- dec-year)))
;; We're moving backwards, and month is backward, so
;; leave it.
((and (< month dec-month) (= dir ?-))
(list month dec-year)))))
(abs-days-now (calendar-absolute-from-gregorian `(,dec-month
,dec-day
,dec-year)))
(abs-days-then (calendar-nth-named-absday nth dayname
act-month
act-year
act-day)))
;; Return the same arguments as `org-edna--read-date-get-relative' above.
(list (- abs-days-then abs-days-now) "d" rel)))))
(defun org-edna--determine-timestamp-format (thing old-ts)
;; Returns the argument to pass to `org-timestamp-format':
;; t for long format (with time), nil for short format (no time).
;; thing is a symbol: year, month, day, hour, minute
;; old-ts is a timestamp string for the current entry
(let* ((spec-ts-format (org-entry-get nil "EDNA_TS_FORMAT")))
(cond
;; An old timestamp exists, so use that format.
(old-ts
;; Returns t for long, nil for short, as we do.
(org-timestamp-has-time-p
(org-timestamp-from-string old-ts)))
;; If THING is minutes or hours, then a timestamp is required.
((memq thing '(minute hour)) t)
;; User specified the EDNA_TS_FORMAT property, so use it.
(spec-ts-format
(pcase spec-ts-format
("long" t)
("short" nil)
(_ (error "Unknown Edna timestamp format %s; expected \"long\" or \"short\"" spec-ts-format))))
;; Fallback to customizable variable.
(t
(pcase org-edna-timestamp-format
(`long t)
(`short nil)
(_ (error "Invalid value for org-edna-timestamp-format %s; expected 'long or 'short"
org-edna-timestamp-format)))))))
(defun org-edna--handle-planning (type last-entry args)
"Handle planning of type TYPE.
LAST-ENTRY is a marker to the source entry.
ARGS is a list of arguments; currently, only the first is used."
(let* ((arg (nth 0 args))
(last-ts (org-with-point-at last-entry (org-edna--get-planning-info type)))
(this-ts (org-edna--get-planning-info type))
(this-time (and this-ts (org-time-string-to-time this-ts)))
(current (org-current-time))
(type-map '(("y" . year)
("m" . month)
("d" . day)
("h" . hour)
("M" . minute))))
(cond
((member arg '(rm remove "rm" "remove"))
(org-add-planning-info nil nil type))
((member arg '(cp copy "cp" "copy"))
(unless last-ts
(error "Tried to copy but last entry doesn't have a timestamp"))
;; Copy old time verbatim
(org-add-planning-info type last-ts))
((string-match-p "\\`float " arg)
(pcase-let* ((`(,n ,what-string ,def) (org-edna--float-time arg this-time current))
(what (cdr (assoc-string what-string type-map)))
(ts-format (org-edna--determine-timestamp-format what this-ts))
(current-ts (format-time-string (org-time-stamp-format ts-format) current))
(ts (if def current-ts this-ts)))
(org--deadline-or-schedule nil type (org-edna--mod-timestamp ts n what))))
((string-match-p "\\`[+-]" arg)
;; Starts with a + or -, so assume we're incrementing a timestamp
;; We support hours and minutes, so this must be supported separately,
;; since org-read-date-analyze doesn't
(pcase-let* ((`(,n ,what-string ,def) (org-edna--read-date-get-relative arg this-time current))
(what (cdr (assoc-string what-string type-map)))
(ts-format (org-edna--determine-timestamp-format what this-ts))
(current-ts (format-time-string (org-time-stamp-format ts-format) current))
(ts (if def current-ts this-ts)))
;; Ensure that the source timestamp exists
(unless ts
(error "Tried to increment a non-existent timestamp"))
(org--deadline-or-schedule nil type (org-edna--mod-timestamp ts n what))))
(t
;; For everything else, assume `org-read-date-analyze' can handle it
;; The third argument to `org-read-date-analyze' specifies the defaults to
;; use if that time component isn't specified. Since there's no way to
;; tell if a time was specified, tell `org-read-date-analyze' to use nil
;; if no time is found.
(let* ((case-fold-search t)
(parsed-time (org-read-date-analyze arg this-time '(nil nil nil nil nil nil)))
(have-time (nth 2 parsed-time))
(final-time (apply 'encode-time (mapcar (lambda (e) (or e 0)) parsed-time)))
(new-ts (format-time-string (if have-time "%F %R" "%F") final-time)))
(org--deadline-or-schedule nil type new-ts))))))
(defun org-edna-action/scheduled! (last-entry &rest args)
"Action to set the scheduled time of a target heading based on ARGS.
Edna Syntax: scheduled!(\"DATE[ TIME]\") [1]
Edna Syntax: scheduled!(rm|remove) [2]
Edna Syntax: scheduled!(cp|copy) [3]
Edna Syntax: scheduled!(\"[+|-|++|--]NTHING[ [+|-]LANDING]\") [4]
Edna Syntax: scheduled!(\"float [+|-|++|--]?N DAYNAME [ DAY[ MONTH]]\") [5]
In form 1, schedule the target for the given date and time. If
DATE is a weekday instead of a date, schedule the target for the
following weekday. If it is a date, schedule it for that date
exactly. TIME is a time string, such as HH:MM. If it isn't
specified, only a date will be applied to the target. Any string
recognized by `org-read-date' may be used.
Form 2 will remove the scheduled time from the target.
Form 3 will copy the scheduled time from LAST-ENTRY (the current
heading) to the target.
Form 4 increments(+) or decrements(-) the target's scheduled time
by N THINGS relative to either itself (+/-) or the current
time (++/--). THING is one of y (years), m (months), d (days),
h (hours), or M (minutes), and N is an integer.
Form 4 may also include a \"landing\" specification. This is
either (a) a day of the week (\"Sun\", \"friday\", etc.), (b)
\"weekday\" or \"wkdy\", or (c) \"weekend\" or \"wknd\".
If (a), then the target date will be adjusted forward (+) or
backward (-) to find the closest target day of the week.
Form (b) will adjust the target time to find a weekday, and (c)
does the same, but for weekends.
Form 5 handles \"float\" time, named for `diary-float'. This
form will set the target's scheduled time to the date of the Nth
DAYNAME after/before MONTH DAY. MONTH may be a month string or
an integer. Use 0 or leave blank for the following or previous
month. DAY is an optional integer. If not given, it will be
1 (for forward) or the last day of MONTH (backward).
For information on how the new timestamp format is chosen when
using ++, see `org-edna-timestamp-format'."
(org-edna--handle-planning 'scheduled last-entry args))
(defun org-edna-action/deadline! (last-entry &rest args)
"Action to set the deadline time of a target heading based on ARGS.
Edna Syntax: deadline!(\"DATE[ TIME]\") [1]
Edna Syntax: deadline!(rm|remove) [2]
Edna Syntax: deadline!(cp|copy) [3]
Edna Syntax: deadline!(\"[+|-|++|--]NTHING[ [+|-]LANDING]\") [4]
Edna Syntax: deadline!(\"float [+|-|++|--]?N DAYNAME [ DAY[ MONTH]]\") [5]
In form 1, set the deadline the target for the given date and
time. If DATE is a weekday instead of a date, set the deadline
the target for the following weekday. If it is a date, set the
deadline it for that date exactly. TIME is a time string, such
as HH:MM. If it isn't specified, only a date will be applied to
the target. Any string recognized by `org-read-date' may be
used.
Form 2 will remove the deadline time from the target.
Form 3 will copy the deadline time from LAST-ENTRY (the current
heading) to the target.
Form 4 increments(+) or decrements(-) the target's deadline time
by N THINGS relative to either itself (+/-) or the current
time (++/--). THING is one of y (years), m (months), d (days),
h (hours), or M (minutes), and N is an integer.
Form 4 may also include a \"landing\" specification. This is
either (a) a day of the week (\"Sun\", \"friday\", etc.), (b)
\"weekday\" or \"wkdy\", or (c) \"weekend\" or \"wknd\".
If (a), then the target date will be adjusted forward (+) or
backward (-) to find the closest target day of the week.
Form (b) will adjust the target time to find a weekday, and (c)
does the same, but for weekends.
Form 5 handles \"float\" time, named for `diary-float'. This
form will set the target's scheduled time to the date of the Nth
DAYNAME after/before MONTH DAY. MONTH may be a month string or
an integer. Use 0 or leave blank for the following or previous
month. DAY is an optional integer. If not given, it will be
1 (for forward) or the last day of MONTH (backward).
For information on how the new timestamp format is chosen when
using ++, see `org-edna-timestamp-format'."
(org-edna--handle-planning 'deadline last-entry args))
(defun org-edna-action/tag! (_last-entry tags)
"Action to set the tags of a target heading to TAGS.
Edna Syntax: tag!(\"TAGS\")
TAGS is a valid tag specification, such as \":aa:bb:cc:\"."
(org-set-tags tags))
(defun org-edna--string-is-numeric-p (string)
"Return non-nil if STRING is a valid numeric string.
Examples of valid numeric strings are \"1\", \"-3\", or \"123\"."
;; Can't use string-to-number, because it returns 0 if STRING isn't a
;; number, which is ambiguous.
(numberp (car (read-from-string string))))
(defun org-edna--increment-numeric-property (pom property &optional decrement)
"Return the incremented value of PROPERTY at POM.
If optional argument DECREMENT is non-nil, decrement the property
value instead."
(let* ((prop-value (org-entry-get pom property)))
(unless prop-value
(error "Attempted to increment/decrement unset property %s" property))
(unless (org-edna--string-is-numeric-p prop-value)
(error "Property %s doesn't have a numeric value (got %s)" property prop-value))
(number-to-string (+ (if decrement -1 1) (string-to-number prop-value)))))
(defun org-edna--cycle-property (pom property &optional previous)
"Cycle the property PROPERTY at POM through its allowed values.
Change PROPERTY to the next allowed value, unless PREVIOUS is
non-nil, in which case, cycle to the previous allowed value."
(let* ((prop-value (org-entry-get pom property)))
(unless prop-value
(error "Attempted to cycle an unset property %s" property))
(save-excursion
;; Jump to the property line, (required for `org-property-next-allowed-value')
(re-search-forward (org-re-property property nil nil prop-value))
(org-property-next-allowed-value previous))))
(defun org-edna-action/set-property! (_last-entry property value)
"Action to set the property PROPERTY of a target heading to VALUE.
Edna Syntax: set-property!(\"PROPERTY\" \"VALUE\")
PROPERTY and VALUE are both strings. PROPERTY must be a valid
org mode property."
(pcase value
((pred stringp)
(org-entry-put (point) property value))
((or `inc `dec)
(let* ((new-value (org-edna--increment-numeric-property (point) property
(eq value 'dec))))
(org-entry-put (point) property new-value)))
((or `next `prev `previous)
(org-edna--cycle-property (point) property (memq value '(prev previous))))))
(defun org-edna-action/delete-property! (_last-entry property)
"Action to delete a property from a target heading.
Edna Syntax: delete-property!(\"PROPERTY\")
PROPERTY must be a valid org mode property."
(org-entry-delete nil property))
(defun org-edna-action/clock-in! (_last-entry)
"Action to clock into a target heading.
Edna Syntax: clock-in!"
(org-clock-in))
(defun org-edna-action/clock-out! (_last-entry)
"Action to clock out from the current clocked heading.
Edna Syntax: clock-out!
Note that this will not necessarily clock out of the target, but
the actual running clock."
(org-clock-out))
(defun org-edna-action/set-priority! (_last-entry priority-action)
"Action to set the priority of a target heading.
Edna Syntax: set-priority!(\"PRIORITY-STRING\") [1]
Edna Syntax: set-priority!(up) [2]
Edna Syntax: set-priority!(down) [3]
Edna Syntax: set-priority!(P) [4]
Form 1 sets the priority to PRIORITY-STRING, so PRIORITY-STRING
must be a valid priority string, such as \"A\" or \"E\". It may
also be the string \" \", which removes the priority from the
target.
Form 2 cycles the target's priority up through the list of
allowed priorities.
Form 3 cycles the target's priority down through the list of
allowed priorities.
Form 4: Set the target's priority to the character P."
(org-priority (if (stringp priority-action)
(string-to-char priority-action)
priority-action)))
(defun org-edna-set-effort (value increment)
"Set the effort property of the current entry.
With numerical prefix arg, use the nth allowed value, 0 stands for the
10th allowed value.
When INCREMENT is non-nil, set the property to the next allowed value."
;; NOTE: Copied from `org-set-effort', because the signature changed in 9.1.7.
;; Since the Org repo doesn't change its version string until after a release,
;; there's no way to tell when to use the old or new signature until after
;; 9.1.7 is released. Therefore, we cut out the middle man and slap the
;; entire function here.
(interactive "P")
(when (equal value 0) (setq value 10))
(let* ((completion-ignore-case t)
(prop org-effort-property)
(cur (org-entry-get nil prop))
(allowed (org-property-get-allowed-values nil prop 'table))
(existing (mapcar 'list (org-property-values prop)))
rpl
(val (cond
((stringp value) value)
((and allowed (integerp value))
(or (car (nth (1- value) allowed))
(car (org-last allowed))))
((and allowed increment)
(or (cl-caadr (member (list cur) allowed))
(user-error "Allowed effort values are not set")))
(allowed
(message "Select 1-9,0, [RET%s]: %s"
(if cur (concat "=" cur) "")
(mapconcat 'car allowed " "))
(setq rpl (read-char-exclusive))
(if (equal rpl ?\r)
cur
(setq rpl (- rpl ?0))
(when (equal rpl 0) (setq rpl 10))
(if (and (> rpl 0) (<= rpl (length allowed)))
(car (nth (1- rpl) allowed))
(org-completing-read "Effort: " allowed nil))))
(t
(org-completing-read
(concat "Effort" (and cur (string-match "\\S-" cur)
(concat " [" cur "]"))
": ")
existing nil nil "" nil cur)))))
(unless (equal (org-entry-get nil prop) val)
(org-entry-put nil prop val))
(org-refresh-property
'((effort . identity)
(effort-minutes . org-duration-to-minutes))
val)
(message "%s is now %s" prop val)))
(defun org-edna-action/set-effort! (_last-entry value)
"Action to set the effort of a target heading.
Edna Syntax: set-effort!(VALUE) [1]
Edna Syntax: set-effort!(increment) [2]
For form 1, set the effort based on VALUE. If VALUE is a string,
it's converted to an integer. Otherwise, the integer is used as
the raw value for the effort.
For form 2, increment the effort to the next allowed value."
(if (eq value 'increment)
(org-edna-set-effort nil value)
(org-edna-set-effort value nil)))
(defun org-edna-action/archive! (_last-entry)
"Action to archive a target heading.
Edna Syntax: archive!
If `org-edna-prompt-for-archive', prompt before archiving the
entry."
(if org-edna-prompt-for-archive
(org-archive-subtree-default-with-confirmation)
(org-archive-subtree-default)))
(defun org-edna-action/chain! (last-entry property)
"Action to copy a property to a target heading.
Edna Syntax: chain!(\"PROPERTY\")
Copy PROPERTY from the source heading to the target heading.
Does nothing if the source heading has no property PROPERTY."
(when-let* ((old-prop (org-entry-get last-entry property)))
(org-entry-put nil property old-prop)))
;;; Conditions
;; For most conditions, we return true if condition is true and neg is false, or
;; if condition is false and neg is true:
;; | cond | neg | res |
;; |------+-----+-----|
;; | t | t | f |
;; | t | f | t |
;; | f | t | t |
;; | f | f | f |
;; This means that we want to take the exclusive-or of condition and neg.
(defun org-edna-condition/done? (neg)
"Condition to check if all target headings are in the DONE state.
Edna Syntax: done?
DONE state is determined by the local value of
`org-done-keywords'.
Example:
* TODO Heading
:PROPERTIES:
:BLOCKER: match(\"target\") done?
:END:
Here, \"Heading\" will block if all targets tagged \"target\" are
in a DONE state.
* TODO Heading 2
:PROPERTIES:
:BLOCKER: match(\"target\") !done?
:END:
Here, \"Heading 2\" will block if all targets tagged \"target\"
are not in a DONE state."
(when-let* ((condition
(if neg
(member (org-entry-get nil "TODO") org-not-done-keywords)
(member (org-entry-get nil "TODO") org-done-keywords))))
(org-get-heading)))
(defun org-edna-condition/todo-state? (neg state)
"Condition to check if all target headings have the TODO state STATE.
Edna Syntax: todo-state?(\"STATE\")
Block the source heading if all target headings have TODO state
STATE. STATE must be a valid TODO state string."
(let ((condition (string-equal (org-entry-get nil "TODO") state)))
(when (org-xor condition neg)
(org-get-heading))))
;; Block if there are headings
(defun org-edna-condition/headings? (neg)
"Condition to check if a target has headings in its file.
Edna Syntax: headings?
Block the source heading if any headings can be found in its
file. This means that target does not have to be a heading."
(let ((condition (not (seq-empty-p (org-map-entries (lambda nil t))))))
(when (org-xor condition neg)
(buffer-name))))
(defun org-edna-condition/variable-set? (neg var val)
"Condition to check if a variable is set in a target.
Edna Syntax: variable-set?(VAR VAL)
Evaluate VAR when visiting a target, and compare it with `equal'
against VAL. Block the source heading if VAR = VAL.
Target does not have to be a heading."
(let ((condition (equal (symbol-value var) val)))
(when (org-xor condition neg)
(format "%s %s= %s" var (if neg "!" "=") val))))
(defun org-edna-condition/has-property? (neg prop val)
"Condition to check if a target heading has property PROP = VAL.
Edna Syntax: has-property?(\"PROP\" \"VAL\")
Block if the target heading has the property PROP set to VAL,
both of which must be strings."
(let ((condition (string-equal (org-entry-get nil prop) val)))
(when (org-xor condition neg)
(org-get-heading))))
(defun org-edna-condition/re-search? (neg match)
"Condition to check for a regular expression in a target's file.
Edna Syntax: re-search?(\"MATCH\")
Block if regular expression MATCH can be found in target's file,
starting from target's position."
(let ((condition (re-search-forward match nil t)))
(when (org-xor condition neg)
(format "%s %s in %s" (if neg "Did Not Find" "Found") match (buffer-name)))))
(defun org-edna-condition/has-tags? (neg &rest tags)
"Check if the target heading has tags.
Edna Syntax: has-tags?(\"tag1\" \"tag2\"...)
Block if the target heading has any of the tags tag1, tag2, etc."
(let* ((condition (apply 'org-edna-entry-has-tags-p tags)))
(when (org-xor condition neg)
(org-get-heading))))
(defun org-edna--heading-matches (match-string)
"Return non-nil if the current heading matches MATCH-STRING."
(let* ((matcher (cdr (org-make-tags-matcher match-string)))
(todo (org-entry-get nil "TODO"))
(tags (org-get-tags nil t))
(level (org-reduced-level (org-outline-level))))
(funcall matcher todo tags level)))
(defun org-edna-condition/matches? (neg match-string)
"Matches a heading against a match string.
Edna Syntax: matches?(\"MATCH-STRING\")
Blocks if the target heading matches MATCH-STRING.
MATCH-STRING is a valid match string as passed to
`org-map-entries'."
(let* ((condition (org-edna--heading-matches match-string)))
(when (org-xor condition neg)
(org-get-heading))))
;;; Consideration
(defun org-edna-handle-consideration (consideration blocks)
"Handle consideration CONSIDERATION.
Edna Syntax: consider(any) [1]
Edna Syntax: consider(N) [2]
Edna Syntax: consider(P) [3]
Edna Syntax: consider(all) [4]
A blocker can be read as:
\"If ANY heading in TARGETS matches CONDITION, block this heading\"
The consideration is \"ANY\".
Form 1 blocks only if any target matches the condition. This is
the default.
Form 2 blocks only if at least N targets meet the condition. N=1
is the same as 'any'.
Form 3 blocks only if *at least* fraction P of the targets meet
the condition. This should be a decimal value between 0 and 1.
Form 4 blocks only if all targets match the condition.
The default consideration is \"any\".
If CONSIDERATION is nil, default to 'any.
The \"consideration\" keyword is also provided. It functions the
same as \"consider\"."
;; BLOCKS is a list of entries that meets the blocking condition; if one isn't
;; blocked, its entry will be nil.
(let* ((consideration (or consideration 'any))
(first-block (seq-find #'identity blocks))
(total-blocks (seq-length blocks))
(fulfilled (seq-count #'not blocks))
(blocked (- total-blocks fulfilled)))
(pcase consideration
('any
;; In order to pass, all of them must be fulfilled, so find the first one
;; that isn't.
first-block)
('all
;; All of them must be set to block, so if one of them doesn't block, the
;; entire entry won't block.
(if (> fulfilled 0)
;; Have one fulfilled
nil
;; None of them are fulfilled
first-block))
((pred integerp)
;; A minimum number of them must meet the blocking condition, so check
;; how many block.
(if (>= blocked consideration)
first-block
nil))
((pred floatp)
;; A certain percentage of them must block for the blocker to block.
(let* ((float-blocked (/ (float blocked) (float total-blocks))))
(if (>= float-blocked consideration)
first-block
nil))))))
;;; Popout editing
(defvar org-edna-edit-original-marker nil)
(defvar org-edna-blocker-section-marker nil)
(defvar org-edna-trigger-section-marker nil)
(defcustom org-edna-edit-buffer-name "*Org Edna Edit Blocker/Trigger*"
"Name of the popout buffer for editing blockers/triggers."
:type 'string
:group 'org-edna)
(defun org-edna-in-edit-buffer-p ()
"Return non-nil if inside the Edna edit buffer."
(string-equal (buffer-name) org-edna-edit-buffer-name))
(defun org-edna-replace-newlines (string)
"Replace newlines with spaces in STRING."
(string-join (split-string string "\n" t) " "))
(defun org-edna-edit-text-between-markers (first-marker second-marker)
"Collect the text between FIRST-MARKER and SECOND-MARKER."
(buffer-substring (marker-position first-marker)
(marker-position second-marker)))
(defun org-edna-edit-blocker-section-text ()
"Collect the BLOCKER section text from an edit buffer."
(when (org-edna-in-edit-buffer-p)
(let ((original-text (org-edna-edit-text-between-markers
org-edna-blocker-section-marker
org-edna-trigger-section-marker)))
;; Strip the BLOCKER key
(when (string-match "^BLOCKER\n\\(\\(?:.*\n\\)+\\)" original-text)
(org-edna-replace-newlines (match-string 1 original-text))))))
(defun org-edna-edit-trigger-section-text ()
"Collect the TRIGGER section text from an edit buffer."
(when (org-edna-in-edit-buffer-p)
(let ((original-text (org-edna-edit-text-between-markers
org-edna-trigger-section-marker
(point-max-marker))))
;; Strip the TRIGGER key
(when (string-match "^TRIGGER\n\\(\\(?:.*\n\\)+\\)" original-text)
(org-edna-replace-newlines (match-string 1 original-text))))))
(defvar org-edna-edit-map
(let ((map (make-sparse-keymap)))
(org-defkey map "\C-x\C-s" 'org-edna-edit-finish)
(org-defkey map "\C-c\C-s" 'org-edna-edit-finish)
(org-defkey map "\C-c\C-c" 'org-edna-edit-finish)
(org-defkey map "\C-c'" 'org-edna-edit-finish)
(org-defkey map "\C-c\C-q" 'org-edna-edit-abort)
(org-defkey map "\C-c\C-k" 'org-edna-edit-abort)
map))
(defun org-edna-edit ()
"Edit the blockers and triggers for current heading in a separate buffer."
(interactive)
;; Move to the start of the current heading
(let* ((heading-point (save-excursion
(org-back-to-heading)
(point-marker)))
(blocker (or (org-entry-get heading-point "BLOCKER") ""))
(trigger (or (org-entry-get heading-point "TRIGGER") ""))
(wc (current-window-configuration))
(sel-win (selected-window)))
(org-switch-to-buffer-other-window org-edna-edit-buffer-name)
(erase-buffer)
;; Keep global-font-lock-mode from turning on font-lock-mode
(let ((font-lock-global-modes '(not fundamental-mode)))
(fundamental-mode))
(use-local-map org-edna-edit-map)
(setq-local font-lock-global-modes (list 'not major-mode))
(setq-local org-edna-edit-original-marker heading-point)
(setq-local org-window-configuration wc)
(setq-local org-selected-window sel-win)
(setq-local org-finish-function 'org-edna-edit-finish)
(insert "Edit blockers and triggers in this buffer under their respective sections below.
All lines under a given section will be merged into one when saving back to
the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'\n\n")
(setq-local org-edna-blocker-section-marker (point-marker))
(insert (format "BLOCKER\n%s\n\n" blocker))
(setq-local org-edna-trigger-section-marker (point-marker))
(insert (format "TRIGGER\n%s\n\n" trigger))
;; Change syntax table to make ! and ? symbol constituents
(modify-syntax-entry ?! "_")
(modify-syntax-entry ?? "_")
;; Set up completion
(add-hook 'completion-at-point-functions 'org-edna-completion-at-point nil t)))
(defun org-edna-edit-finish ()
"Finish an Edna property edit."
(interactive)
(let ((blocker (org-edna-edit-blocker-section-text))
(trigger (org-edna-edit-trigger-section-text))
(pos-marker org-edna-edit-original-marker)
(wc org-window-configuration)
(sel-win org-selected-window))
(set-window-configuration wc)
(select-window sel-win)
(goto-char pos-marker)
(unless (string-empty-p blocker)
(org-entry-put nil "BLOCKER" blocker))
(unless (string-empty-p trigger)
(org-entry-put nil "TRIGGER" trigger))
(kill-buffer org-edna-edit-buffer-name)))
(defun org-edna-edit-abort ()
"Abort an Edna property edit."
(interactive)
(let ((pos-marker org-edna-edit-original-marker)
(wc org-window-configuration)
(sel-win org-selected-window))
(set-window-configuration wc)
(select-window sel-win)
(goto-char pos-marker)
(kill-buffer org-edna-edit-buffer-name)))
;;; Completion
(defun org-edna-between-markers-p (point first-marker second-marker)
"Return non-nil if POINT is between FIRST-MARKER and SECOND-MARKER in the current buffer."
(and (markerp first-marker)
(markerp second-marker)
(eq (marker-buffer first-marker)
(marker-buffer second-marker))
(eq (current-buffer) (marker-buffer first-marker))
(<= (marker-position first-marker) point)
(>= (marker-position second-marker) point)))
(defun org-edna-edit-in-blocker-section-p ()
"Return non-nil if `point' is in an edna blocker edit section."
(org-edna-between-markers-p (point)
org-edna-blocker-section-marker
org-edna-trigger-section-marker))
(defun org-edna-edit-in-trigger-section-p ()
"Return non-nil if `point' is in an edna trigger edit section."
(org-edna-between-markers-p (point)
org-edna-trigger-section-marker
(point-max-marker)))
(defun org-edna--collect-keywords (keyword-type &optional suffix)
"Collect known Edna keywords of type KEYWORD-TYPE.
SUFFIX is an additional suffix to use when matching keywords."
(let* ((suffix (or suffix ""))
(edna-sym-list)
(edna-rx (rx-to-string `(and
string-start
"org-edna-"
,keyword-type
"/"
(submatch (one-or-more ascii))
,suffix
string-end))))
(mapatoms
(lambda (s)
(when (and (string-match edna-rx (symbol-name s)) (fboundp s))
(cl-pushnew (concat (match-string-no-properties 1 (symbol-name s)) suffix)
edna-sym-list))))
edna-sym-list))
(defun org-edna--collect-finders ()
"Return a list of finder keywords."
(org-edna--collect-keywords "finder"))
(defun org-edna--collect-actions ()
"Return a list of action keywords."
(org-edna--collect-keywords "action" "!"))
(defun org-edna--collect-conditions ()
"Return a list of condition keywords."
(org-edna--collect-keywords "condition" "?"))
(defun org-edna-completions-for-blocker ()
"Return a list of all allowed Edna keywords for a blocker."
`(,@(org-edna--collect-finders)
,@(org-edna--collect-conditions)
"consideration" "consider"))
(defun org-edna-completions-for-trigger ()
"Return a list of all allowed Edna keywords for a trigger."
`(,@(org-edna--collect-finders)
,@(org-edna--collect-actions)))
(defun org-edna-completion-table-function (string pred action)
"Completion table function for Edna keywords.
See `minibuffer-completion-table' for description of STRING,
PRED, and ACTION."
(let ((completions (cond
;; Don't offer completion inside of arguments
((> (syntax-ppss-depth (syntax-ppss)) 0) nil)
((org-edna-edit-in-blocker-section-p)
(org-edna-completions-for-blocker))
((org-edna-edit-in-trigger-section-p)
(org-edna-completions-for-trigger)))))
(pcase action
(`nil
(try-completion string completions pred))
(`t
(all-completions string completions pred))
(`lambda
(test-completion string completions pred))
(`(boundaries . _) nil)
(`metadata
`(metadata . ((category . org-edna)
(annotation-function . nil)
(display-sort-function . identity)
(cycle-sort-function . identity)))))))
(defun org-edna-completion-at-point ()
"Complete the Edna keyword at point."
(when-let* ((bounds (bounds-of-thing-at-point 'symbol)))
(list (car bounds) (cdr bounds) 'org-edna-completion-table-function)))
(defun org-edna-describe-keyword (keyword)
"Describe the Org Edna keyword KEYWORD.
KEYWORD should be a string for a keyword recognized by edna.
Displays help for KEYWORD in the Help buffer."
(interactive
(list
(completing-read
"Keyword: "
`(,@(org-edna--collect-finders)
,@(org-edna--collect-actions)
,@(org-edna--collect-conditions)
"consideration" "consider")
nil ;; No filter predicate
t))) ;; require match
;; help-split-fundoc splits the usage info from the rest of the documentation.
;; This avoids having another usage line in the keyword documentation that has
;; nothing to do with how edna expects the function.
(pcase-let* ((`(,_type . ,func) (org-edna--function-for-key (intern keyword)))
(`(,_usage . ,doc) (help-split-fundoc (documentation func t) func)))
(with-help-window (help-buffer)
(princ doc))))
;;; Bug Reports
(declare-function lm-report-bug "lisp-mnt" (topic))
(defun org-edna-submit-bug-report (topic)
"Submit a bug report to the Edna developers.
TOPIC is the topic for the bug report."
(interactive "sTopic: ")
(require 'lisp-mnt)
(let* ((src-file (locate-library "org-edna.el" t))
(src-buf-live (find-buffer-visiting src-file))
(src-buf (find-file-noselect src-file)))
(with-current-buffer src-buf
(lm-report-bug topic))
;; Kill the buffer if it wasn't live
(unless src-buf-live
(kill-buffer src-buf))))
(provide 'org-edna)
;;; org-edna.el ends here
org-edna-1.0.2/org-edna-pkg.el 0000644 0001752 0001753 00000000412 13425536424 014441 0 ustar elpa elpa ;; Generated package description from org-edna.el
(define-package "org-edna" "1.0.2" "Extensible Dependencies 'N' Actions" '((emacs "25.1") (seq "2.19") (org "9.0.5")) :url "https://savannah.nongnu.org/projects/org-edna-el/" :keywords '("convenience" "text" "org"))
org-edna-1.0.2/org-edna-tests.org 0000644 0001752 0001753 00000023730 13425536050 015214 0 ustar elpa elpa #+STARTUP: nologdone
#+STARTUP: indent
#+PROPERTY: Effort_ALL 0:01 0:02 0:03
#+PROPERTY: COUNTER_ALL a b c d
* COMMENT Copying
Copyright (C) 2017-2018 Free Software Foundation, Inc.
#+BEGIN_QUOTE
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
#+END_QUOTE
* Test Pool
** TODO Tagged Heading 1 :1:test:
:PROPERTIES:
:ID: 0fa0d4dd-40f2-4251-a558-4c6e2898c2df
:END:
** TODO Tagged Heading 2 :1:test:
:PROPERTIES:
:ID: 30957f69-8c31-4a13-86ff-f0c5026fb65d
:END:
** TODO ID Heading 1
:PROPERTIES:
:ID: 0d491588-7da3-43c5-b51a-87fbd34f79f7
:LOGGING: nil
:END:
** TODO ID Heading 2
:PROPERTIES:
:ID: b010cbad-60dc-46ef-a164-eb155e62cbb2
:LOGGING: nil
:END:
** TODO ID Heading 3
DEADLINE: <2000-01-15 Sat 00:00> SCHEDULED: <2000-01-15 Sat 00:00>
:PROPERTIES:
:ID: 97e6b0f0-40c4-464f-b760-6e5ca9744eb5
:END:
<2000-01-15 Sat 00:00>
** DONE ID Heading 4
:PROPERTIES:
:ID: 7d4d564b-18b2-445c-a0c8-b1b3fb9ad29e
:END:
** Scheduled Headings
*** TODO Scheduled Heading 1
DEADLINE: <2017-01-01 Sun> SCHEDULED: <2017-01-01 Sun>
:PROPERTIES:
:ID: caf27724-0887-4565-9765-ed2f1edcfb16
:END:
<2017-01-01 Sun>
*** TODO Scheduled Heading 2
DEADLINE: <2017-01-01 Sun> SCHEDULED: <2017-01-01 Sun>
:PROPERTIES:
:ID: 5594d4f1-b1bb-400f-9f3d-e2f9b43e82c3
:END:
<2017-01-01 Sun>
** Sibling Headings
:PROPERTIES:
:ID: 21b8f1f5-14e8-4677-873d-69e0389fdc9e
:END:
*** Sibling 1
:PROPERTIES:
:ID: 82a4ac3d-9565-4f94-bc84-2bbfd8d7d96c
:END:
*** Sibling 2
:PROPERTIES:
:ID: 72534efa-e932-460b-ae2d-f044a0074815
:END:
*** Sibling 3
:PROPERTIES:
:ID: 06aca55e-ce09-46df-80d7-5b52e55d6505
:END:
** Archive
*** TODO Archive One
:PROPERTIES:
:ID: d7668277-f959-43ba-8e85-8a3c76996862
:END:
* Finder Tests
** Match
*** TODO Blocking Test
:PROPERTIES:
:BLOCKER: match("test&1")
:ID: caccd0a6-d400-410a-9018-b0635b07a37e
:LOGGING: nil
:END:
* Relatives Tests
:PROPERTIES:
:ID: c07cf4c1-3693-443a-9d79-b581f7cbd62c
:END:
** Parent Heading #1
:PROPERTIES:
:ID: 5a35daf7-4957-4588-9a68-21d8763a9e0d
:END:
*** [#C] Standard Child Heading
DEADLINE: <2017-01-07 Sat> SCHEDULED: <2017-01-02 Mon>
:PROPERTIES:
:ID: 7c542695-8165-4c8b-b44d-4c12fa009548
:Effort: 0:01
:END:
<2017-01-02 Mon>
*** [#B] Child Heading with Children
DEADLINE: <2017-01-03 Tue> SCHEDULED: <2017-01-03 Tue>
:PROPERTIES:
:ID: c7a986df-8d89-4509-b086-6db429b5607b
:Effort: 0:03
:END:
<2017-01-03 Tue>
**** Child Heading One
:PROPERTIES:
:ID: 588bbd29-2e07-437f-b74d-f72459b545a1
:END:
**** Child Heading Two
:PROPERTIES:
:ID: a7047c81-21ec-46cd-8289-60ad515900ff
:END:
*** TODO [#A] Child Heading with TODO
DEADLINE: <2017-01-01 Sun> SCHEDULED: <2017-01-06 Fri>
:PROPERTIES:
:ID: 8c0b31a1-af49-473c-92ea-a5c1c3bace33
:Effort: 0:02
:END:
<2017-01-06 Fri>
*** [#B] COMMENT Commented Child Heading
DEADLINE: <2017-01-08 Sun> SCHEDULED: <2017-01-04 Wed>
:PROPERTIES:
:ID: 0a1b9508-17ce-49c5-8ff3-28a0076374f5
:Effort: 0:06
:END:
<2017-01-04 Wed>
*** [#A] Archived Child Heading :ARCHIVE:
DEADLINE: <2017-01-02 Mon> SCHEDULED: <2017-01-01 Sun>
:PROPERTIES:
:ID: a4b6131e-0560-4201-86d5-f32b36363431
:Effort: 0:05
:END:
<2017-01-01 Sun>
*** DONE [#C] Child Heading with DONE
DEADLINE: <2017-01-05 Thu> SCHEDULED: <2017-01-05 Thu>
:PROPERTIES:
:ID: 4a1d74a2-b032-47da-a823-b32f5cab0aae
:Effort: 0:08
:END:
<2017-01-05 Thu>
** Parent Sub Heading #2
:PROPERTIES:
:ID: 4fe67f03-2b35-4708-8c38-54d2c4dfab81
:END:
* Documentation Tests
** Ancestors
:PROPERTIES:
:ID: 24a0c3bb-7e69-4e9e-bb98-5aba2ff17bb1
:END:
*** TODO Heading 1
**** TODO Heading 2
**** TODO Heading 3
***** TODO Heading 4
****** TODO Heading 5
:PROPERTIES:
:BLOCKER: ancestors
:END:
** Descendants
:PROPERTIES:
:ID: cc18dc74-00e8-4081-b46f-e36800041fe7
:END:
*** TODO Heading 1
:PROPERTIES:
:BLOCKER: descendants
:END:
**** TODO Heading 2
**** TODO Heading 3
***** TODO Heading 4
****** TODO Heading 5
** Laundry
:PROPERTIES:
:ID: e57ce099-9f37-47f4-a6bb-61a84eb1fbbe
:END:
*** TODO Put clothes in washer
SCHEDULED: <2000-01-15 Sat 00:00>
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:END:
*** TODO Put clothes in dryer
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("Sun 9:00")
:BLOCKER: previous-sibling
:END:
*** TODO Fold laundry
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
*** TODO Put clothes away
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
** Nightlies - Standard
:PROPERTIES:
:ID: 8b6d9820-d943-4622-85c9-4a346e033453
:END:
*** TODO Nightly
DEADLINE: <2000-01-15 Sat +1d>
:PROPERTIES:
:ID: 2d94abf9-2d63-46fd-8dc5-cd396555bcfe
:BLOCKER: match("nightly")
:TRIGGER: match("nightly") todo!(TODO)
:END:
*** TODO Prepare Tomorrow's Lunch :nightly:
:PROPERTIES:
:TRIGGER: if match("nightly") then ids(2d94abf9-2d63-46fd-8dc5-cd396555bcfe) todo!(DONE) endif
:END:
*** TODO Lock Back Door :nightly:
:PROPERTIES:
:TRIGGER: if match("nightly") then ids(2d94abf9-2d63-46fd-8dc5-cd396555bcfe) todo!(DONE) endif
:END:
*** TODO Feed Dog :nightly:
:PROPERTIES:
:TRIGGER: if match("nightly") then ids(2d94abf9-2d63-46fd-8dc5-cd396555bcfe) todo!(DONE) endif
:END:
** Dailies - Consideration
:PROPERTIES:
:ID: 630805bb-a864-4cdc-9a6f-0f126e887c66
:END:
*** TODO Daily
DEADLINE: <2000-01-15 Sat +1d>
:PROPERTIES:
:ID: 96f7e46c-40c3-4f5b-8f00-81a6e3cb122b
:TRIGGER: match("daily") todo!(TODO)
:END:
*** TODO Prepare Tomorrow's Lunch :daily:
:PROPERTIES:
:TRIGGER: if consider(all) match("daily") then ids(96f7e46c-40c3-4f5b-8f00-81a6e3cb122b) todo!(DONE) endif
:END:
*** TODO Lock Back Door :daily:
:PROPERTIES:
:TRIGGER: if consider(3) match("daily") then ids(96f7e46c-40c3-4f5b-8f00-81a6e3cb122b) todo!(DONE) endif
:END:
*** TODO Feed Dog :daily:
:PROPERTIES:
:TRIGGER: if consider(0.9) match("daily") then ids(96f7e46c-40c3-4f5b-8f00-81a6e3cb122b) todo!(DONE) endif
:END:
** Weeklies - Inverted Conditional
:PROPERTIES:
:ID: cf529a5e-1b0c-40c3-8f85-fe2fc4df0ffd
:END:
*** TODO Weekly
DEADLINE: <2000-01-15 Sat +1d>
:PROPERTIES:
:ID: 9a0c4b00-64be-4971-a93e-c530cbdd4b2b
:TRIGGER: match("weekly") todo!(TODO)
:END:
*** TODO Prepare Tomorrow's Lunch :weekly:
:PROPERTIES:
:TRIGGER: if match("weekly") then else ids(9a0c4b00-64be-4971-a93e-c530cbdd4b2b) todo!(DONE) endif
:END:
*** TODO Lock Back Door :weekly:
:PROPERTIES:
:TRIGGER: if match("weekly") then else ids(9a0c4b00-64be-4971-a93e-c530cbdd4b2b) todo!(DONE) endif
:END:
*** TODO Feed Dog :weekly:
:PROPERTIES:
:TRIGGER: if match("weekly") then else ids(9a0c4b00-64be-4971-a93e-c530cbdd4b2b) todo!(DONE) endif
:END:
** Basic Shower - No Conditional
:PROPERTIES:
:ID: 34d67756-927b-4a21-a62d-7989bd138946
:END:
*** TODO Take Shower
:PROPERTIES:
:COUNT: 0
:TRIGGER: self set-property!("COUNT" inc) todo!("TODO")
:END:
*** TODO Wash Towels
:PROPERTIES:
:BLOCKER: previous-sibling !has-property?("COUNT" "3")
:TRIGGER: previous-sibling set-property!("COUNT" "0")
:END:
** Snow Shoveling
:PROPERTIES:
:ID: b1d89bd8-db96-486e-874c-98e2b3a8cbf2
:END:
*** TODO Shovel on Monday
*** TODO Shovel on Tuesday
*** TODO Shovel on Wednesday
*** TODO Put shovel away
:PROPERTIES:
:BLOCKER: consider(all) rest-of-siblings-wrap
:END:
** Work I - Consider Fraction
:PROPERTIES:
:ID: 7de5af8b-a226-463f-8360-edd88b99462a
:END:
*** TODO Shovel Snow
*** TODO Clean room
*** TODO Vacuum
*** TODO Eat lunch
*** TODO Work on Edna
:PROPERTIES:
:BLOCKER: consider(0.5) rest-of-siblings-wrap
:END:
** Work II - Consider Number
:PROPERTIES:
:ID: b79279f7-be3c-45ac-96dc-6e962a5873d4
:END:
*** TODO Shovel Snow
*** TODO Clean room
*** TODO Vacuum
*** TODO Eat lunch
*** TODO Work on Edna
:PROPERTIES:
:BLOCKER: consider(2) rest-of-siblings-wrap
:END:
** Has Tags
:PROPERTIES:
:ID: 6885e932-2c3e-4f20-ac22-5f5a0e791d67
:END:
*** Task 1 :tag1:
*** Task 2 :tag3:tag2:
*** TODO Task 3
:PROPERTIES:
:BLOCKER: rest-of-siblings-wrap has-tags?("tag1" "tag2")
:END:
** Matches
:PROPERTIES:
:ID: 8170bf82-c2ea-49e8-bd79-97a95176783f
:END:
*** TODO Task 1
*** TODO Task 2
*** TODO Task 3
:PROPERTIES:
:BLOCKER: rest-of-siblings-wrap !matches?("TODO==\"DONE\"")
:END:
** Chain
:PROPERTIES:
:ID: 1bd282ea-9238-47ea-9b4d-dafba19d278b
:END:
*** TODO Heading 1
:PROPERTIES:
:COUNT: 2
:TRIGGER: next-sibling chain!("COUNT")
:END:
*** TODO Heading 2
** Multiple Blockers
:PROPERTIES:
:ID: 61e754c2-f292-42b5-8166-e4298dc190d6
:END:
*** TODO Heading 1
:PROPERTIES:
:ID: 1942caf2-caad-4757-b689-3c0029c1d8a5
:END:
*** TODO Heading 2
*** TODO Heading 3
:PROPERTIES:
:BLOCKER: previous-sibling !done? ids(1942caf2-caad-4757-b689-3c0029c1d8a5) !done?
:END:
* User Examples
** Test to show undesired time spec added to generated SCHEDULED datestamp
:PROPERTIES:
:ID: 5b63293c-23ef-40e7-ad8e-093e4c1e1464
:END:
*** TODO task 1
DEADLINE: <2019-02-15 Fri +1y -2w>
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++7d")
:END:
*** TODO task 2
*** TODO task 3
org-edna-1.0.2/ChangeLog 0000644 0001752 0001753 00000003040 13425536221 013411 0 ustar elpa elpa 2019-02-02 Ian Dunn
Merge commit '5289910f860a9676b1ab9dd450e8ff82308e91a4'
2018-12-09 Ian Dunn
Merge commit '5ed8901691b26eb5847f9522d20fa93a4fbaace8'
2018-11-25 Ian Dunn
Merge commit '2c5ac0cb808ae6953fbc74cc497245dafb51051f'
2018-11-11 Ian Dunn
Merge commit 'bec310f1a793e58bfd31a12a612e7ffe931505d0'
2018-11-11 Ian Dunn
Merge commit '26b0a6e6fb03fe4b895bb03972bbb0d80ce2e8e3'
2018-02-07 Ian Dunn
Merge commit '1e10278c3201f55c339a81e33b2ba8e8adf989b6'
2018-02-04 Ian Dunn
Merge commit '35acd8c2f9710e54ec3f433ae4af40cfff635295'
2017-12-27 Ian Dunn
Merge commit 'dca9cb6d19f9e28ad8fa267a84710703ff38e006'
2017-12-23 Ian Dunn
Merge commit '556e03be1068d746e3d672185987c433302229fa'
2017-12-17 Ian Dunn
Merge commit '1f8935e117918fab8c6ad700b627a67ac4a4f10c'
2017-12-17 Ian Dunn
Merge commit 'ea0f9fb914cccc1d127eea94bc4c607dbcd4dc7d'
2017-05-29 Ian Dunn
Merge commit '50657b33a26d77cd224621e07ff616527f671bdb'
2017-05-28 Ian Dunn
Merge commit '7f863bd82effad75b108c191266d79391e2a4439' into
scratch/org-edna
2017-05-21 Ian Dunn
Add 'packages/org-edna/' from commit
'ce425a365cc305142a6ec7ec95ebe5fd636100e3'
git-subtree-dir: packages/org-edna git-subtree-mainline:
07e5ab2a06f86a779cba517735cdb9c06c6f287b git-subtree-split:
ce425a365cc305142a6ec7ec95ebe5fd636100e3
org-edna-1.0.2/COPYING 0000644 0001752 0001753 00000104513 13112635331 012674 0 ustar elpa elpa GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
org-edna-1.0.2/org-edna-tests.el 0000644 0001752 0001753 00000277430 13425536050 015035 0 ustar elpa elpa ;;; org-edna-tests.el --- Tests for org-edna
;; Copyright (C) 2017-2018 Free Software Foundation, Inc.
;; Author: Ian Dunn
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free Software
;; Foundation; either version 3, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
;; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
;; details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see .
;;; Commentary:
;;; Code:
(require 'org-edna)
(require 'ert)
(require 'org-id)
(defvar org-edna-test-inhibit-messages nil
"Whether to inhibit messages (apart from ERT messages).")
(defconst org-edna-test-dir
(expand-file-name (file-name-directory (or load-file-name buffer-file-name))))
(defconst org-edna-test-file
(expand-file-name "org-edna-tests.org" org-edna-test-dir))
(defconst org-edna-tests-el
(expand-file-name "org-edna-tests.el" org-edna-test-dir))
;; Jan 15, 2000; chosen at random
(defconst org-edna-test-time
(encode-time 0 0 0 15 1 2000))
(defconst org-edna-test-sibling-one-id "82a4ac3d-9565-4f94-bc84-2bbfd8d7d96c")
(defconst org-edna-test-sibling-two-id "72534efa-e932-460b-ae2d-f044a0074815")
(defconst org-edna-test-sibling-three-id "06aca55e-ce09-46df-80d7-5b52e55d6505")
(defconst org-edna-test-parent-id "21b8f1f5-14e8-4677-873d-69e0389fdc9e")
(defconst org-edna-test-id-heading-one "0d491588-7da3-43c5-b51a-87fbd34f79f7")
(defconst org-edna-test-id-heading-two "b010cbad-60dc-46ef-a164-eb155e62cbb2")
(defconst org-edna-test-id-heading-three "97e6b0f0-40c4-464f-b760-6e5ca9744eb5")
(defconst org-edna-test-id-heading-four "7d4d564b-18b2-445c-a0c8-b1b3fb9ad29e")
(defconst org-edna-test-archive-heading "d7668277-f959-43ba-8e85-8a3c76996862")
(defconst org-edna-test-relative-grandparent "c07cf4c1-3693-443a-9d79-b581f7cbd62c")
(defconst org-edna-test-relative-parent-one "5a35daf7-4957-4588-9a68-21d8763a9e0d")
(defconst org-edna-test-relative-parent-two "4fe67f03-2b35-4708-8c38-54d2c4dfab81")
(defconst org-edna-test-relative-standard-child "7c542695-8165-4c8b-b44d-4c12fa009548")
(defconst org-edna-test-relative-child-with-children "c7a986df-8d89-4509-b086-6db429b5607b")
(defconst org-edna-test-relative-grandchild-one "588bbd29-2e07-437f-b74d-f72459b545a1")
(defconst org-edna-test-relative-grandchild-two "a7047c81-21ec-46cd-8289-60ad515900ff")
(defconst org-edna-test-relative-child-with-todo "8c0b31a1-af49-473c-92ea-a5c1c3bace33")
(defconst org-edna-test-relative-commented-child "0a1b9508-17ce-49c5-8ff3-28a0076374f5")
(defconst org-edna-test-relative-archived-child "a4b6131e-0560-4201-86d5-f32b36363431")
(defconst org-edna-test-relative-child-with-done "4a1d74a2-b032-47da-a823-b32f5cab0aae")
(defun org-edna-test-restore-test-file ()
"Restore the test file back to its original state."
(with-current-buffer (get-file-buffer org-edna-test-file)
(revert-buffer nil t)))
(defmacro org-edna-protect-test-file (&rest body)
(declare (indent 0))
`(unwind-protect
(progn ,@body)
;; Change the test file back to its original state.
(org-edna-test-restore-test-file)))
(defmacro org-edna-test-setup (&rest body)
"Common settings for tests."
(declare (indent 0))
;; Override `current-time' so we can get a deterministic value
`(cl-letf* (((symbol-function 'current-time) (lambda () org-edna-test-time))
;; Only use the test file in the agenda
(org-agenda-files `(,org-edna-test-file))
;; Ensure interactive modification of TODO states works.
(org-todo-keywords '((sequence "TODO" "|" "DONE")))
;; Only block based on Edna
(org-blocker-hook 'org-edna-blocker-function)
;; Only trigger based on Edna
(org-trigger-hook 'org-edna-trigger-function)
;; Inhibit messages if indicated
(inhibit-message org-edna-test-inhibit-messages))
,@body))
(defmacro org-edna-with-point-at-test-heading (heading-id &rest body)
(declare (indent 1))
`(org-with-point-at (org-edna-find-test-heading ,heading-id)
,@body))
(defmacro org-edna-with-test-heading (heading-id &rest body)
"Establish a test case with test heading HEADING-ID.
HEADING-ID is a UUID string of a heading to use.
Moves point to the heading, protects the test file, sets default
test settings, then runs BODY."
(declare (indent 1))
`(org-edna-test-setup
(org-edna-protect-test-file
(org-edna-with-point-at-test-heading ,heading-id
,@body))))
(defun org-edna-find-test-heading (id)
"Find the test heading with id ID.
This avoids org-id digging into its internal database."
(org-id-find-id-in-file id org-edna-test-file t))
;; _test exists to give more detailed reports in ERT output.
(defun org-edna-test-compare-todos (pom expected-state _test)
(string-equal (org-entry-get pom "TODO") expected-state))
(defun org-edna-test-change-todo-state (pom new-state)
(org-with-point-at pom (org-todo new-state)))
(defun org-edna-test-check-block (pom _test)
"Check if the heading at point-or-marker POM is blocked."
(org-edna-test-change-todo-state pom "DONE")
(org-edna-test-compare-todos pom "TODO" _test))
(defun org-edna-test-mark-done (&rest poms)
"Mark all points-or-markers in POMS as DONE."
(dolist (pom poms)
(org-edna-test-change-todo-state pom "DONE")))
(defun org-edna-test-mark-todo (&rest poms)
"Mark all points-or-markers in POMS as TODO."
(dolist (pom poms)
(org-edna-test-change-todo-state pom "TODO")))
(defun org-edna-test-children-marks ()
(org-edna-collect-descendants nil))
;;; Parser Tests
(ert-deftest org-edna-parse-form-no-arguments ()
(let* ((input-string "test-string")
(parsed (org-edna-parse-string-form input-string)))
(should parsed)
(should (= (length parsed) 2))
(pcase-let* ((`((,key . ,args) ,pos) parsed))
(should (eq key 'test-string))
(should (not args))
(should (= pos 11)))))
(ert-deftest org-edna-parse-form-no-arguments-modifier ()
(let* ((input-string "!test-string")
(parsed (org-edna-parse-string-form input-string)))
(should parsed)
(should (= (length parsed) 2))
(pcase-let* ((`((,key . ,args) ,pos) parsed))
(should (eq key '!test-string))
(should (not args))
(should (= pos 12)))))
(ert-deftest org-edna-parse-form-single-argument ()
(let* ((input-string "test-string(abc)")
(parsed (org-edna-parse-string-form input-string)))
(should parsed)
(should (= (length parsed) 2))
(pcase-let* ((`((,key . ,args) ,pos) parsed))
(should (eq key 'test-string))
(should (= (length args) 1))
(should (symbolp (nth 0 args)))
(should (eq (nth 0 args) 'abc))
(should (= pos (length input-string))))))
(ert-deftest org-edna-parse-form-string-argument ()
(let* ((input-string "test-string(abc \"def (ghi)\")")
(parsed (org-edna-parse-string-form input-string)))
(should parsed)
(should (= (length parsed) 2))
(pcase-let* ((`((,key . ,args) ,pos) parsed))
(should (eq key 'test-string))
(should (= (length args) 2))
(should (symbolp (nth 0 args)))
(should (eq (nth 0 args) 'abc))
(should (stringp (nth 1 args)))
(should (string-equal (nth 1 args) "def (ghi)"))
(should (= pos (length input-string))))))
(ert-deftest org-edna-parse-form-multiple-forms ()
(let ((input-string "test-string1 test-string2")
pos)
(pcase-let* ((`((,key1 . ,args1) ,pos1) (org-edna-parse-string-form input-string)))
(should (eq key1 'test-string1))
(should (not args1))
(should (= pos1 13))
(setq pos pos1))
(pcase-let* ((`((,key2 . ,args2) ,pos2) (org-edna-parse-string-form (substring input-string pos))))
(should (eq key2 'test-string2))
(should (not args2))
(should (= pos2 12)))))
(ert-deftest org-edna-parse-form-empty-argument-list ()
(let ((input-string "test-string1()"))
(pcase-let* ((`((,key1 ,args1) ,pos1) (org-edna-parse-string-form input-string)))
(should (eq key1 'test-string1))
(should (not args1))
(should (= pos1 (length input-string))))))
(ert-deftest org-edna-parse-form-condition ()
(let ((input-string "variable-set?()"))
(pcase-let* ((`((,key1 . ,args1) ,pos1) (org-edna-parse-string-form input-string))
(`(,modifier1 . ,key1) (org-edna-break-modifier key1))
(`(,type . ,func) (org-edna--function-for-key key1)))
(should (eq key1 'variable-set?))
(should (not args1))
(should (not modifier1))
(should (= pos1 (length input-string)))
(should (eq type 'condition))
(should (eq func 'org-edna-condition/variable-set?)))))
(ert-deftest org-edna-form-to-sexp-no-arguments ()
(let* ((input-string "self")
(sexp (org-edna-string-form-to-sexp-form input-string 'condition)))
(should (equal
sexp
'(((self)
(!done?)))))))
(ert-deftest org-edna-form-to-sexp-negation ()
(let* ((input-string "self !done?")
(sexp (org-edna-string-form-to-sexp-form input-string 'condition)))
(should (equal
sexp
'(((self)
(!done?)))))))
(ert-deftest org-edna-form-to-sexp-arguments ()
(let* ((input-string "match(\"checklist\") todo!(TODO)")
(sexp (org-edna-string-form-to-sexp-form input-string 'action)))
(should (equal
sexp
'(((match "checklist")
(todo! TODO)))))))
(ert-deftest org-edna-form-to-sexp-if-no-else ()
(let* ((input-string "if match(\"checklist\") done? then self todo!(TODO) endif")
(sexp (org-edna-string-form-to-sexp-form input-string 'action)))
(should (equal
sexp
'(((if (((match "checklist")
(done?)))
(((self)
(todo! TODO)))
nil)))))))
(ert-deftest org-edna-form-to-sexp-if-else ()
(let* ((input-string "if match(\"checklist\") done? then self todo!(TODO) else siblings todo!(DONE) endif")
(sexp (org-edna-string-form-to-sexp-form input-string 'action)))
(should (equal
sexp
'(((if (((match "checklist")
(done?)))
(((self)
(todo! TODO)))
(((siblings)
(todo! DONE))))))))))
(ert-deftest org-edna-form-to-sexp-if-multiple-thens ()
(let* ((input-string "if match(\"checklist\") done? then self next-sibling todo!(TODO) self set-property!(\"COUNTER\" \"0\") endif")
(sexp (org-edna-string-form-to-sexp-form input-string 'action)))
(should (equal
sexp
'(((if (((match "checklist")
(done?)))
(((self)
(next-sibling)
(todo! TODO))
((self)
(set-property! "COUNTER" "0")))
nil)))))))
(ert-deftest org-edna-form-to-sexp-if-multiple-elses ()
(let* ((input-string "if match(\"checklist\") done? then self todo!(TODO) else siblings todo!(DONE) self todo!(TODO) endif")
(sexp (org-edna-string-form-to-sexp-form input-string 'action)))
(should (equal
sexp
'(((if (((match "checklist")
(done?)))
(((self)
(todo! TODO)))
(((siblings)
(todo! DONE))
((self)
(todo! TODO))))))))))
(ert-deftest org-edna-form-to-sexp-failed-if ()
(pcase-let* ((input-string "if match(\"checklist\") done?")
(`(,error . ,data) (should-error (org-edna-string-form-to-sexp-form
input-string 'action)
:type 'invalid-read-syntax)))
(should (eq error 'invalid-read-syntax))
(should (listp data))
(should (eq (length data) 6))
(should (string-equal (plist-get data :msg) "Malformed if-construct; expected then terminator"))
;; Error should point to the start of the if-statement
(should (eq (plist-get data :error-pos) 0))))
(ert-deftest org-edna-form-to-sexp-failed-if-then ()
(pcase-let* ((input-string "if match(\"checklist\") done? then")
(`(,error . ,data) (should-error (org-edna-string-form-to-sexp-form
input-string 'action)
:type 'invalid-read-syntax)))
(should (eq error 'invalid-read-syntax))
(should (listp data))
(should (eq (length data) 6))
(should (string-equal (plist-get data :msg)
"Malformed if-construct; expected else or endif terminator"))
;; Error should point to the start of the if-statement
(should (eq (plist-get data :error-pos) 28))))
(ert-deftest org-edna-form-to-sexp-failed-if-then-else ()
(pcase-let* ((input-string "if match(\"checklist\") done? then todo!(TODO) else todo!(TODO)")
(`(,error . ,data) (should-error (org-edna-string-form-to-sexp-form
input-string 'action)
:type 'invalid-read-syntax)))
(should (eq error 'invalid-read-syntax))
(should (listp data))
(should (eq (length data) 6))
(should (string-equal (plist-get data :msg)
"Malformed if-construct; expected endif terminator"))
;; Error should point to the start of the if-statement
(should (eq (plist-get data :error-pos) 45))))
;;; Finders
(defsubst org-edna-heading (pom)
(org-with-point-at pom
(org-get-heading t t t t)))
(ert-deftest org-edna-finder/match-single-arg ()
(org-edna-test-setup
(let* ((targets (org-edna-finder/match "test&1")))
(should (= (length targets) 2))
(should (string-equal (org-edna-heading (nth 0 targets)) "Tagged Heading 1"))
(should (string-equal (org-edna-heading (nth 1 targets)) "Tagged Heading 2")))))
(ert-deftest org-edna-finder/ids-single ()
(org-edna-test-setup
(let* ((test-id "caccd0a6-d400-410a-9018-b0635b07a37e")
(targets (org-edna-finder/ids test-id)))
(should (= (length targets) 1))
(should (string-equal (org-edna-heading (nth 0 targets)) "Blocking Test"))
(should (string-equal (org-entry-get (nth 0 targets) "ID") test-id)))))
(ert-deftest org-edna-finder/ids-multiple ()
(org-edna-test-setup
(let* ((test-ids '("0d491588-7da3-43c5-b51a-87fbd34f79f7"
"b010cbad-60dc-46ef-a164-eb155e62cbb2"))
(targets (apply 'org-edna-finder/ids test-ids)))
(should (= (length targets) 2))
(should (string-equal (org-edna-heading (nth 0 targets)) "ID Heading 1"))
(should (string-equal (org-entry-get (nth 0 targets) "ID") (nth 0 test-ids)))
(should (string-equal (org-edna-heading (nth 1 targets)) "ID Heading 2"))
(should (string-equal (org-entry-get (nth 1 targets) "ID") (nth 1 test-ids))))))
(ert-deftest org-edna-finder/match-blocker ()
(org-edna-test-setup
(let* ((heading (org-edna-find-test-heading "caccd0a6-d400-410a-9018-b0635b07a37e"))
(blocker (org-entry-get heading "BLOCKER"))
blocking-entry)
(should (string-equal "match(\"test&1\")" blocker))
(org-with-point-at heading
(setq blocking-entry (org-edna-process-form blocker 'condition)))
(should (string-equal (substring-no-properties blocking-entry)
"TODO Tagged Heading 1 :1:test:")))))
(ert-deftest org-edna-finder/file ()
(let* ((targets (org-edna-finder/file org-edna-test-file)))
(should (= (length targets) 1))
(should (markerp (nth 0 targets)))
(org-with-point-at (nth 0 targets)
(should (equal (current-buffer) (find-buffer-visiting org-edna-test-file)))
(should (equal (point) 1)))))
(ert-deftest org-edna-finder/org-file ()
(let* ((org-directory (file-name-directory org-edna-test-file))
(targets (org-edna-finder/org-file (file-name-nondirectory org-edna-test-file))))
(should (= (length targets) 1))
(should (markerp (nth 0 targets)))
(org-with-point-at (nth 0 targets)
(should (equal (current-buffer) (find-buffer-visiting org-edna-test-file)))
(should (equal (point) 1)))))
(ert-deftest org-edna-finder/self ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading "82a4ac3d-9565-4f94-bc84-2bbfd8d7d96c"))
(targets (org-with-point-at current (org-edna-finder/self))))
(should (= (length targets) 1))
(should (equal current (nth 0 targets)))))
(ert-deftest org-edna-finder/siblings ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-sibling-one-id))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
`(,org-edna-test-sibling-one-id
,org-edna-test-sibling-two-id
,org-edna-test-sibling-three-id)))
(targets (org-with-point-at current
(org-edna-finder/siblings))))
(should (equal siblings targets))))
(ert-deftest org-edna-finder/siblings-wrap ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading "72534efa-e932-460b-ae2d-f044a0074815"))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
'("06aca55e-ce09-46df-80d7-5b52e55d6505"
"82a4ac3d-9565-4f94-bc84-2bbfd8d7d96c")))
(targets (org-with-point-at current
(org-edna-finder/siblings-wrap))))
(should (= (length targets) 2))
(should (equal siblings targets))))
(ert-deftest org-edna-finder/rest-of-siblings ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading "72534efa-e932-460b-ae2d-f044a0074815"))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
'("06aca55e-ce09-46df-80d7-5b52e55d6505")))
(targets (org-with-point-at current
(org-edna-finder/rest-of-siblings))))
(should (= (length targets) 1))
(should (equal siblings targets))))
(ert-deftest org-edna-finder/next-sibling ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading "72534efa-e932-460b-ae2d-f044a0074815"))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
'("06aca55e-ce09-46df-80d7-5b52e55d6505")))
(targets (org-with-point-at current
(org-edna-finder/next-sibling))))
(should (= (length targets) 1))
(should (equal siblings targets))))
(ert-deftest org-edna-finder/next-sibling-wrap-next ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-sibling-two-id))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
`(,org-edna-test-sibling-three-id)))
(targets (org-with-point-at current
(org-edna-finder/next-sibling-wrap))))
(should (= (length targets) 1))
(should (equal siblings targets))))
(ert-deftest org-edna-finder/next-sibling-wrap-wrap ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-sibling-three-id))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
`(,org-edna-test-sibling-one-id)))
(targets (org-with-point-at current
(org-edna-finder/next-sibling-wrap))))
(should (= (length targets) 1))
(should (equal siblings targets))))
(ert-deftest org-edna-finder/previous-sibling ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading "06aca55e-ce09-46df-80d7-5b52e55d6505"))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
'("72534efa-e932-460b-ae2d-f044a0074815")))
(targets (org-with-point-at current
(org-edna-finder/previous-sibling))))
(should (= (length targets) 1))
(should (equal siblings targets))))
(ert-deftest org-edna-finder/first-child ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-parent-id))
(first-child (list (org-edna-find-test-heading org-edna-test-sibling-one-id)))
(targets (org-with-point-at current
(org-edna-finder/first-child))))
(should (= (length targets) 1))
(should (equal first-child targets))))
(ert-deftest org-edna-finder/children ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-parent-id))
(children (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
`(,org-edna-test-sibling-one-id
,org-edna-test-sibling-two-id
,org-edna-test-sibling-three-id)))
(targets (org-with-point-at current
(org-edna-finder/children))))
(should (= (length targets) 3))
(should (equal children targets))))
(ert-deftest org-edna-finder/parent ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-sibling-one-id))
(parent (list (org-edna-find-test-heading org-edna-test-parent-id)))
(targets (org-with-point-at current
(org-edna-finder/parent))))
(should (= (length targets) 1))
(should (equal parent targets))))
(ert-deftest org-edna-relatives/from-top ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-sibling-one-id))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
`(,org-edna-test-sibling-one-id)))
(targets (org-with-point-at current
(org-edna-finder/relatives 'from-top 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/from-bottom ()
(let* ((org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading org-edna-test-sibling-one-id))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
`(,org-edna-test-sibling-three-id)))
(targets (org-with-point-at current
(org-edna-finder/relatives 'from-bottom 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/forward-wrap-no-wrap ()
(let* ((start-marker org-edna-test-sibling-one-id)
(target-list `(,org-edna-test-sibling-two-id))
(arg 'forward-wrap)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/forward-wrap-wrap ()
(let* ((start-marker org-edna-test-sibling-three-id)
(target-list `(,org-edna-test-sibling-one-id))
(arg 'forward-wrap)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/forward-no-wrap-no-wrap ()
(let* ((start-marker org-edna-test-sibling-one-id)
(target-list `(,org-edna-test-sibling-two-id))
(arg 'forward-no-wrap)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/forward-no-wrap-wrap ()
(let* ((start-marker org-edna-test-sibling-three-id)
(target-list nil)
(arg 'forward-no-wrap)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/backward-wrap-no-wrap ()
(let* ((start-marker org-edna-test-sibling-three-id)
(target-list `(,org-edna-test-sibling-two-id))
(arg 'backward-wrap)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/backward-wrap-wrap ()
(let* ((start-marker org-edna-test-sibling-one-id)
(target-list `(,org-edna-test-sibling-three-id))
(arg 'backward-wrap)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/backward-no-wrap-no-wrap ()
(let* ((start-marker org-edna-test-sibling-three-id)
(target-list `(,org-edna-test-sibling-two-id))
(arg 'backward-no-wrap)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/backward-no-wrap-wrap ()
(let* ((start-marker org-edna-test-sibling-one-id)
(target-list nil)
(arg 'backward-no-wrap)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/walk-up ()
(let* ((start-marker org-edna-test-sibling-one-id)
(target-list `(,org-edna-test-parent-id))
(arg 'walk-up)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/walk-up-with-self ()
(let* ((start-marker org-edna-test-sibling-one-id)
(target-list `(,org-edna-test-sibling-one-id))
(arg 'walk-up-with-self)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/walk-down ()
(let* ((start-marker org-edna-test-parent-id)
(target-list `(,org-edna-test-sibling-one-id))
(arg 'walk-down)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/walk-down-with-self ()
(let* ((start-marker org-edna-test-parent-id)
(target-list `(,org-edna-test-parent-id))
(arg 'walk-down-with-self)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/walk-down ()
(let* ((start-marker org-edna-test-parent-id)
(target-list `(,org-edna-test-sibling-one-id))
(arg 'walk-down)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 1))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/walk-down-full ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-standard-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-grandchild-one
,org-edna-test-relative-grandchild-two
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-commented-child
,org-edna-test-relative-archived-child
,org-edna-test-relative-child-with-done))
(arg 'walk-down)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/step-down-full ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-standard-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-commented-child
,org-edna-test-relative-archived-child
,org-edna-test-relative-child-with-done))
(arg 'step-down)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/filter-todo-only ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-todo))
(arg 'step-down)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 'todo-only))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/filter-todo-and-done-only ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-todo
,org-edna-test-relative-child-with-done))
(arg 'step-down)
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg 'todo-and-done-only))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/filter-no-comments ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-standard-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-archived-child
,org-edna-test-relative-child-with-done))
(arg 'step-down)
(filter 'no-comment)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg filter size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/filter-no-archive ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-standard-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-commented-child
,org-edna-test-relative-child-with-done))
(arg 'step-down)
(filter 'no-archive)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg filter size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/filter-has-tag ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-archived-child))
(arg 'step-down)
(filter "+ARCHIVE")
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg filter size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/filter-no-tag ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-standard-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-commented-child
,org-edna-test-relative-child-with-done))
(arg 'step-down)
(filter "-ARCHIVE")
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg filter size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/filter-matches-regexp ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-children
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-child-with-done))
(arg 'step-down)
(filter "Child Heading With .*")
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg filter size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/sort-reverse ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-done
,org-edna-test-relative-archived-child
,org-edna-test-relative-commented-child
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-child-with-children
,org-edna-test-relative-standard-child))
(arg 'step-down)
(sort 'reverse-sort)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets (org-with-point-at current
(org-edna-finder/relatives arg sort size))))
(should (equal siblings targets))))
(ert-deftest org-edna-relatives/sort-priority ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-todo
,org-edna-test-relative-archived-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-commented-child
,org-edna-test-relative-standard-child
,org-edna-test-relative-child-with-done))
(arg 'step-down)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list))
(targets ))
(should (equal siblings
(org-with-point-at current
(org-edna-finder/relatives arg 'priority-up size))))
(should (equal (nreverse siblings)
(org-with-point-at current
(org-edna-finder/relatives arg 'priority-down size))))))
(ert-deftest org-edna-relatives/sort-effort ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-done
,org-edna-test-relative-commented-child
,org-edna-test-relative-archived-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-child-with-todo
,org-edna-test-relative-standard-child))
(arg 'step-down)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list)))
(should (equal siblings
(org-with-point-at current
(org-edna-finder/relatives arg 'effort-up size))))
(should (equal (nreverse siblings)
(org-with-point-at current
(org-edna-finder/relatives arg 'effort-down size))))))
(ert-deftest org-edna-relatives/sort-scheduled ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-todo
,org-edna-test-relative-child-with-done
,org-edna-test-relative-commented-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-standard-child
,org-edna-test-relative-archived-child))
(arg 'step-down)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list)))
(should (equal siblings
(org-with-point-at current
(org-edna-finder/relatives arg 'scheduled-up size))))
(should (equal (nreverse siblings)
(org-with-point-at current
(org-edna-finder/relatives arg 'scheduled-down size))))))
(ert-deftest org-edna-relatives/sort-deadline ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-commented-child
,org-edna-test-relative-standard-child
,org-edna-test-relative-child-with-done
,org-edna-test-relative-child-with-children
,org-edna-test-relative-archived-child
,org-edna-test-relative-child-with-todo))
(arg 'step-down)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list)))
(should (equal siblings
(org-with-point-at current
(org-edna-finder/relatives arg 'deadline-up size))))
(should (equal (nreverse siblings)
(org-with-point-at current
(org-edna-finder/relatives arg 'deadline-down size))))))
(ert-deftest org-edna-relatives/sort-timestamp ()
(let* ((start-marker org-edna-test-relative-parent-one)
(target-list `(,org-edna-test-relative-child-with-todo
,org-edna-test-relative-child-with-done
,org-edna-test-relative-commented-child
,org-edna-test-relative-child-with-children
,org-edna-test-relative-standard-child
,org-edna-test-relative-archived-child))
(arg 'step-down)
(size (length target-list))
(org-agenda-files `(,org-edna-test-file))
(current (org-edna-find-test-heading start-marker))
(siblings (mapcar
(lambda (uuid) (org-edna-find-test-heading uuid))
target-list)))
(should (equal siblings
(org-with-point-at current
(org-edna-finder/relatives arg 'timestamp-up size))))
(should (equal (nreverse siblings)
(org-with-point-at current
(org-edna-finder/relatives arg 'timestamp-down size))))))
(ert-deftest org-edna-cache/no-entry ()
(let* ((org-edna-finder-use-cache t)
(org-edna--finder-cache (make-hash-table :test 'equal)))
;; Empty, so `org-edna--get-cache-entry' should return nil.
(should (not (org-edna--get-cache-entry 'org-edna-finder/match '("test&1"))))))
(ert-deftest org-edna-cache/added-new-entry ()
(let* ((org-edna-finder-use-cache t)
(org-edna--finder-cache (make-hash-table :test 'equal))
(org-agenda-files `(,org-edna-test-file))
(targets (org-edna--handle-finder 'org-edna-finder/match '("test&1"))))
(should (= (length targets) 2))
(should (string-equal (org-edna-heading (nth 0 targets)) "Tagged Heading 1"))
(should (string-equal (org-edna-heading (nth 1 targets)) "Tagged Heading 2"))
(should (= (hash-table-count org-edna--finder-cache) 1))
;; Verify that we've got a valid cache entry.
(should (org-edna--get-cache-entry 'org-edna-finder/match '("test&1")))
;; Verify that any other signature returns nil.
(should (not (org-edna--get-cache-entry 'org-edna-finder/match '("test&2"))))
(let ((cache-entry (gethash (make-org-edna--finder-input :func-sym 'org-edna-finder/match
:args '("test&1"))
org-edna--finder-cache)))
(should cache-entry)
(should (equal (org-edna--finder-cache-entry-input cache-entry)
(make-org-edna--finder-input :func-sym 'org-edna-finder/match
:args '("test&1"))))
(should (equal (org-edna--finder-cache-entry-results cache-entry)
targets)))))
(ert-deftest org-edna-cache/timed-out ()
(let* ((org-edna-finder-use-cache t)
(org-edna--finder-cache (make-hash-table :test 'equal))
(org-edna-finder-cache-timeout 1) ;; Set timeout to 1 second
(org-agenda-files `(,org-edna-test-file))
(targets (org-edna--handle-finder 'org-edna-finder/match '("test&1")))
;; Time increment required to invalidate a cache entry
(time-increment `(0 ,org-edna-finder-cache-timeout)))
(should (org-edna--get-cache-entry 'org-edna-finder/match '("test&1")))
;; Validate the cache entry
(let ((cache-entry (gethash (make-org-edna--finder-input :func-sym 'org-edna-finder/match
:args '("test&1"))
org-edna--finder-cache)))
(should cache-entry)
(should (equal (org-edna--finder-cache-entry-input cache-entry)
(make-org-edna--finder-input :func-sym 'org-edna-finder/match
:args '("test&1"))))
(should (equal (org-edna--finder-cache-entry-results cache-entry)
targets))
;; Override `current-time' so we can get a deterministic value
;; The value invalidates the cache entry
(cl-letf* (((symbol-function 'current-time)
(lambda () (time-add (org-edna--finder-cache-entry-last-run-time cache-entry)
time-increment))))
(should (not (org-edna--get-cache-entry 'org-edna-finder/match '("test&1"))))))))
;;; Actions
(ert-deftest org-edna-action/todo-test ()
(org-edna-with-test-heading "0d491588-7da3-43c5-b51a-87fbd34f79f7"
(org-edna-action/todo! nil "DONE")
(should (string-equal (org-entry-get nil "TODO") "DONE"))
(org-edna-action/todo! nil "TODO")
(should (string-equal (org-entry-get nil "TODO") "TODO"))
(org-edna-action/todo! nil 'DONE)
(should (string-equal (org-entry-get nil "TODO") "DONE"))
(org-edna-action/todo! nil 'TODO)
(should (string-equal (org-entry-get nil "TODO") "TODO"))))
;; Scheduled
(ert-deftest org-edna-action-scheduled/wkdy ()
(org-edna-with-test-heading "0d491588-7da3-43c5-b51a-87fbd34f79f7"
(org-edna-action/scheduled! nil "Mon")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-17 Mon>"))
(org-edna-action/scheduled! nil 'rm)
(should (not (org-entry-get nil "SCHEDULED")))
(org-edna-action/scheduled! nil "Mon 9:00")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-17 Mon 09:00>"))
(org-edna-action/scheduled! nil 'rm)
(should (not (org-entry-get nil "SCHEDULED")))))
(ert-deftest org-edna-action-scheduled/cp ()
(org-edna-with-test-heading "0d491588-7da3-43c5-b51a-87fbd34f79f7"
(let* ((source (org-edna-find-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"))
(pairs '((cp . rm) (copy . remove) ("cp" . "rm") ("copy" . "remove"))))
(dolist (pair pairs)
(org-edna-action/scheduled! source (car pair))
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))
(org-edna-action/scheduled! source (cdr pair))
(should (not (org-entry-get nil "SCHEDULED")))))))
(ert-deftest org-edna-action-scheduled/inc ()
(org-edna-with-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"
;; Time starts at Jan 15, 2000
(org-edna-action/scheduled! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))
;; Increment 1 minute
(org-edna-action/scheduled! nil "+1M")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:01>"))
;; Decrement 1 minute
(org-edna-action/scheduled! nil "-1M")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))
;; +1 day
(org-edna-action/scheduled! nil "+1d")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-16 Sun 00:00>"))
;; +1 hour from current time
(org-edna-action/scheduled! nil "++1h")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 01:00>"))
;; Back to Saturday
(org-edna-action/scheduled! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))
;; -1 day to Friday
(org-edna-action/scheduled! nil "-1d")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-14 Fri 00:00>"))
;; Increment two days to the next weekday
(org-edna-action/scheduled! nil "+2wkdy")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-17 Mon 00:00>"))
;; Increment one day, expected to land on a weekday
(org-edna-action/scheduled! nil "+1wkdy")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-18 Tue 00:00>"))
;; Move forward 8 days, then backward until we find a weekend
(org-edna-action/scheduled! nil "+8d -wknd")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-23 Sun 00:00>"))
;; Move forward one week, then forward until we find a weekday
;; (org-edna-action/scheduled! nil "+1w +wkdy")
;; (should (string-equal (org-entry-get nil "SCHEDULED")
;; "<2000-01-31 Mon 00:00>"))
;; Back to Saturday for other tests
(org-edna-action/scheduled! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))))
(ert-deftest org-edna-action-scheduled/landing ()
"Test landing arguments to scheduled increment."
(org-edna-with-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"
;; Time starts at Jan 15, 2000
(org-edna-action/scheduled! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))
;; Move forward 10 days, then backward until we find a weekend
(org-edna-action/scheduled! nil "+10d -wknd")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-23 Sun 00:00>"))
;; Move forward one week, then forward until we find a weekday
(org-edna-action/scheduled! nil "+1w +wkdy")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-31 Mon 00:00>"))
;; Back to Saturday for other tests
(org-edna-action/scheduled! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))))
(ert-deftest org-edna-action-scheduled/landing-no-hour ()
"Test landing arguments to scheduled increment, without hour."
(org-edna-with-test-heading "caf27724-0887-4565-9765-ed2f1edcfb16"
;; Time starts at Jan 1, 2017
(org-edna-action/scheduled! nil "2017-01-01 Sun")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2017-01-01 Sun>"))
;; Move forward 10 days, then backward until we find a weekend
(org-edna-action/scheduled! nil "+10d -wknd")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2017-01-08 Sun>"))
;; Move forward one week, then forward until we find a weekday
(org-edna-action/scheduled! nil "+1w +wkdy")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2017-01-16 Mon>"))
;; Back to Saturday for other tests
(org-edna-action/scheduled! nil "2017-01-01 Sun")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2017-01-01 Sun>"))))
(ert-deftest org-edna-action-scheduled/float ()
(org-edna-with-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"
;; Time starts at Jan 15, 2000
(org-edna-action/scheduled! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-01-15 Sat 00:00>"))
;; The third Tuesday of next month (Feb 15th)
(org-edna-action/scheduled! nil "float 3 Tue")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-02-15 Tue 00:00>"))
;; The second Friday of the following May (May 12th)
(org-edna-action/scheduled! nil "float 2 5 May")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-05-12 Fri 00:00>"))
;; Move forward to the second Wednesday of the next month (June 14th)
(org-edna-action/scheduled! nil "float 2 Wednesday")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-06-14 Wed 00:00>"))
;; Move forward to the first Thursday in the following Jan (Jan 4th, 2001)
(org-edna-action/scheduled! nil "float 1 4 Jan")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2001-01-04 Thu 00:00>"))
;; The fourth Monday in Feb, 2000 (Feb 28th)
(org-edna-action/scheduled! nil "float ++4 monday")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-02-28 Mon 00:00>"))
;; The second Monday after Mar 12th, 2000 (Mar 20th)
(org-edna-action/scheduled! nil "float 2 monday Mar 12")
(should (string-equal (org-entry-get nil "SCHEDULED")
"<2000-03-20 Mon 00:00>"))))
(ert-deftest org-edna-action-deadline/wkdy ()
(org-edna-with-test-heading "0d491588-7da3-43c5-b51a-87fbd34f79f7"
(org-edna-action/deadline! nil "Mon")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-17 Mon>"))
(org-edna-action/deadline! nil 'rm)
(should (not (org-entry-get nil "DEADLINE")))
(org-edna-action/deadline! nil "Mon 9:00")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-17 Mon 09:00>"))
(org-edna-action/deadline! nil 'rm)
(should (not (org-entry-get nil "DEADLINE")))))
(ert-deftest org-edna-action-deadline/cp ()
(org-edna-with-test-heading "0d491588-7da3-43c5-b51a-87fbd34f79f7"
(let* ((source (org-edna-find-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"))
(pairs '((cp . rm) (copy . remove) ("cp" . "rm") ("copy" . "remove"))))
(dolist (pair pairs)
(org-edna-action/deadline! source (car pair))
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))
(org-edna-action/deadline! source (cdr pair))
(should (not (org-entry-get nil "DEADLINE")))))))
(ert-deftest org-edna-action-deadline/inc ()
(org-edna-with-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"
;; Time starts at Jan 15, 2000
(org-edna-action/deadline! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))
;; Increment 1 minute
(org-edna-action/deadline! nil "+1M")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:01>"))
;; Decrement 1 minute
(org-edna-action/deadline! nil "-1M")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))
;; +1 day
(org-edna-action/deadline! nil "+1d")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-16 Sun 00:00>"))
;; +1 hour from current time
(org-edna-action/deadline! nil "++1h")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 01:00>"))
;; Back to Saturday
(org-edna-action/deadline! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))
;; -1 day to Friday
(org-edna-action/deadline! nil "-1d")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-14 Fri 00:00>"))
;; Increment two days to the next weekday
(org-edna-action/deadline! nil "+2wkdy")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-17 Mon 00:00>"))
;; Increment one day, expected to land on a weekday
(org-edna-action/deadline! nil "+1wkdy")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-18 Tue 00:00>"))
;; Move forward 8 days, then backward until we find a weekend
(org-edna-action/deadline! nil "+8d -wknd")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-23 Sun 00:00>"))
;; Move forward one week, then forward until we find a weekday
;; (org-edna-action/deadline! nil "+1w +wkdy")
;; (should (string-equal (org-entry-get nil "DEADLINE")
;; "<2000-01-31 Mon 00:00>"))
;; Back to Saturday for other tests
(org-edna-action/deadline! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))))
(ert-deftest org-edna-action-deadline/landing ()
"Test landing arguments to deadline increment."
(org-edna-with-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"
;; Time starts at Jan 15, 2000
(org-edna-action/deadline! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))
;; Move forward 10 days, then backward until we find a weekend
(org-edna-action/deadline! nil "+10d -wknd")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-23 Sun 00:00>"))
;; Move forward one week, then forward until we find a weekday
(org-edna-action/deadline! nil "+1w +wkdy")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-31 Mon 00:00>"))
;; Back to Saturday for other tests
(org-edna-action/deadline! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))))
(ert-deftest org-edna-action-deadline/landing-no-hour ()
"Test landing arguments to deadline increment, without hour."
(org-edna-with-test-heading "caf27724-0887-4565-9765-ed2f1edcfb16"
;; Time starts at Jan 1, 2017
(org-edna-action/deadline! nil "2017-01-01 Sun")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2017-01-01 Sun>"))
;; Move forward 10 days, then backward until we find a weekend
(org-edna-action/deadline! nil "+10d -wknd")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2017-01-08 Sun>"))
;; Move forward one week, then forward until we find a weekday
(org-edna-action/deadline! nil "+1w +wkdy")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2017-01-16 Mon>"))
;; Back to Saturday for other tests
(org-edna-action/deadline! nil "2017-01-01 Sun")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2017-01-01 Sun>"))))
(ert-deftest org-edna-action-deadline/float ()
(org-edna-with-test-heading "97e6b0f0-40c4-464f-b760-6e5ca9744eb5"
;; Time starts at Jan 15, 2000
(org-edna-action/deadline! nil "2000-01-15 Sat 00:00")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-01-15 Sat 00:00>"))
;; The third Tuesday of next month (Feb 15th)
(org-edna-action/deadline! nil "float 3 Tue")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-02-15 Tue 00:00>"))
;; The second Friday of the following May (May 12th)
(org-edna-action/deadline! nil "float 2 5 May")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-05-12 Fri 00:00>"))
;; Move forward to the second Wednesday of the next month (June 14th)
(org-edna-action/deadline! nil "float 2 Wednesday")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-06-14 Wed 00:00>"))
;; Move forward to the first Thursday in the following Jan (Jan 4th, 2001)
(org-edna-action/deadline! nil "float 1 4 Jan")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2001-01-04 Thu 00:00>"))
;; The fourth Monday in Feb, 2000 (Feb 28th)
(org-edna-action/deadline! nil "float ++4 monday")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-02-28 Mon 00:00>"))
;; The second Monday after Mar 12th, 2000 (Mar 20th)
(org-edna-action/deadline! nil "float 2 monday Mar 12")
(should (string-equal (org-entry-get nil "DEADLINE")
"<2000-03-20 Mon 00:00>"))))
(ert-deftest org-edna-action-tag ()
(org-edna-with-test-heading org-edna-test-id-heading-one
(org-edna-action/tag! nil "tag")
(should (equal (org-get-tags) '("tag")))
(org-edna-action/tag! nil "")
(should (equal (org-get-tags) nil))))
(ert-deftest org-edna-action-property ()
(org-edna-with-test-heading org-edna-test-id-heading-one
(org-edna-action/set-property! nil "TEST" "1")
(should (equal (org-entry-get nil "TEST") "1"))
(org-edna-action/delete-property! nil "TEST")
(should-not (org-entry-get nil "TEST"))))
(ert-deftest org-edna-action-property/inc-dec ()
(org-edna-with-test-heading org-edna-test-id-heading-one
(org-edna-action/set-property! nil "TEST" "1")
(should (equal (org-entry-get nil "TEST") "1"))
(org-edna-action/set-property! nil "TEST" 'inc)
(should (equal (org-entry-get nil "TEST") "2"))
(org-edna-action/set-property! nil "TEST" 'dec)
(should (equal (org-entry-get nil "TEST") "1"))
(org-edna-action/delete-property! nil "TEST")
(should-not (org-entry-get nil "TEST"))
(should-error (org-edna-action/set-property! nil "TEST" 'inc))
(should-error (org-edna-action/set-property! nil "TEST" 'dec))
(org-edna-action/set-property! nil "TEST" "a")
(should (equal (org-entry-get nil "TEST") "a"))
(should-error (org-edna-action/set-property! nil "TEST" 'inc))
(should-error (org-edna-action/set-property! nil "TEST" 'dec))
(org-edna-action/delete-property! nil "TEST")
(should-not (org-entry-get nil "TEST"))))
(ert-deftest org-edna-action-property/next-prev ()
(org-edna-with-test-heading org-edna-test-id-heading-one
(org-edna-action/set-property! nil "TEST" "a")
(should (equal (org-entry-get nil "TEST") "a"))
(should-error (org-edna-action/set-property! nil "TEST" 'next))
(should-error (org-edna-action/set-property! nil "TEST" 'prev))
(should-error (org-edna-action/set-property! nil "TEST" 'previous))
(org-edna-action/delete-property! nil "TEST")
(should-not (org-entry-get nil "TEST"))
;; Test moving forwards
(org-edna-action/set-property! nil "COUNTER" "a")
(should (equal (org-entry-get nil "COUNTER") "a"))
(org-edna-action/set-property! nil "COUNTER" 'next)
(should (equal (org-entry-get nil "COUNTER") "b"))
;; Test moving forwards past the last one
(org-edna-action/set-property! nil "COUNTER" "d")
(should (equal (org-entry-get nil "COUNTER") "d"))
(org-edna-action/set-property! nil "COUNTER" 'next)
(should (equal (org-entry-get nil "COUNTER") "a"))
;; Test moving backwards past the first one
(org-edna-action/set-property! nil "COUNTER" 'prev)
(should (equal (org-entry-get nil "COUNTER") "d"))
;; Test moving backwards normally
(org-edna-action/set-property! nil "COUNTER" 'previous)
(should (equal (org-entry-get nil "COUNTER") "c"))
(org-edna-action/delete-property! nil "COUNTER")
(should-not (org-entry-get nil "COUNTER"))))
(ert-deftest org-edna-action-clock ()
(org-edna-with-test-heading org-edna-test-id-heading-one
(org-edna-action/clock-in! nil)
(should (org-clocking-p))
(should (equal org-clock-hd-marker (point-marker)))
(org-edna-action/clock-out! nil)
(should-not (org-clocking-p))))
(ert-deftest org-edna-action-priority ()
(org-edna-with-test-heading org-edna-test-id-heading-one
(let ((org-lowest-priority ?C)
(org-highest-priority ?A)
(org-default-priority ?B))
(org-edna-action/set-priority! nil "A")
(should (equal (org-entry-get nil "PRIORITY") "A"))
(org-edna-action/set-priority! nil 'down)
(should (equal (org-entry-get nil "PRIORITY") "B"))
(org-edna-action/set-priority! nil 'up)
(should (equal (org-entry-get nil "PRIORITY") "A"))
(org-edna-action/set-priority! nil ?C)
(should (equal (org-entry-get nil "PRIORITY") "C"))
(org-edna-action/set-priority! nil 'remove)
(should (equal (org-entry-get nil "PRIORITY") "B")))))
(ert-deftest org-edna-action-effort ()
(org-edna-with-test-heading org-edna-test-id-heading-one
(org-edna-action/set-effort! nil "0:01")
(should (equal (org-entry-get nil "EFFORT") "0:01"))
(org-edna-action/set-effort! nil 'increment)
(should (equal (org-entry-get nil "EFFORT") "0:02"))
(org-entry-delete nil "EFFORT")))
(ert-deftest org-edna-action-archive ()
(org-edna-with-test-heading org-edna-test-archive-heading
(let* ((org-archive-save-context-info '(todo))
;; Archive it to the same location
(org-archive-location "::** Archive")
;; We're non-interactive, so no prompt.
(org-edna-prompt-for-archive nil))
(org-edna-action/archive! nil)
(should (equal (org-entry-get nil "ARCHIVE_TODO") "TODO"))
(org-entry-delete nil "ARCHIVE_TODO"))))
(ert-deftest org-edna-action-chain ()
(org-edna-test-setup
(let ((old-pom (org-edna-find-test-heading org-edna-test-id-heading-one))
(new-pom (org-edna-find-test-heading org-edna-test-id-heading-two)))
(org-edna-protect-test-file
(org-entry-put old-pom "TEST" "1")
(org-with-point-at new-pom
(org-edna-action/chain! old-pom "TEST")
(should (equal (org-entry-get nil "TEST") "1")))
(org-entry-delete old-pom "TEST")
(org-entry-delete new-pom "TEST")))))
;;; Conditions
(defun org-edna-test-condition-form (func-sym pom-true pom-false block-true block-false &rest args)
(org-edna-test-setup
(let* ((block-true (or block-true (org-with-point-at pom-true (org-get-heading))))
(block-false (or block-false (org-with-point-at pom-false (org-get-heading)))))
(org-with-point-at pom-true
(should-not (apply func-sym t args))
(should (equal (apply func-sym nil args) block-true)))
(org-with-point-at pom-false
(should (equal (apply func-sym t args) block-false))
(should-not (apply func-sym nil args))))))
(ert-deftest org-edna-condition-done ()
(let* ((pom-done (org-edna-find-test-heading org-edna-test-id-heading-four))
(pom-todo (org-edna-find-test-heading org-edna-test-id-heading-one))
(block-done (org-with-point-at pom-done (org-get-heading)))
(block-todo (org-with-point-at pom-todo (org-get-heading))))
(org-edna-test-condition-form 'org-edna-condition/done?
pom-done pom-todo
block-done block-todo)))
(ert-deftest org-edna-condition-todo-state-string ()
(let* ((pom-done (org-edna-find-test-heading org-edna-test-id-heading-four))
(pom-todo (org-edna-find-test-heading org-edna-test-id-heading-one))
(block-done (org-with-point-at pom-done (org-get-heading)))
(block-todo (org-with-point-at pom-todo (org-get-heading))))
(org-edna-test-condition-form 'org-edna-condition/todo-state?
pom-todo pom-done
block-todo block-done
"TODO")))
(ert-deftest org-edna-condition-todo-state-symbol ()
(let* ((pom-done (org-edna-find-test-heading org-edna-test-id-heading-four))
(pom-todo (org-edna-find-test-heading org-edna-test-id-heading-one))
(block-done (org-with-point-at pom-done (org-get-heading)))
(block-todo (org-with-point-at pom-todo (org-get-heading))))
(org-edna-test-condition-form 'org-edna-condition/todo-state?
pom-todo pom-done
block-todo block-done
'TODO)))
(ert-deftest org-edna-condition-headings ()
(pcase-let* ((`(,pom-headings ,block-headings)
(with-current-buffer (find-file-noselect org-edna-test-file)
(list (point-min-marker) (buffer-name))))
(`(,pom-no-headings ,block-no-headings)
(with-current-buffer (find-file-noselect org-edna-tests-el)
(list (point-min-marker) (buffer-name)))))
(org-edna-test-condition-form 'org-edna-condition/headings?
pom-headings pom-no-headings
block-headings block-no-headings)))
(ert-deftest org-edna-condition-variable-set ()
(let* ((temp-var t))
(should-not (org-edna-condition/variable-set? t 'temp-var t))
(should (equal (org-edna-condition/variable-set? nil 'temp-var t)
"temp-var == t"))
(should (equal (org-edna-condition/variable-set? t 'temp-var nil)
"temp-var != nil"))
(should-not (org-edna-condition/variable-set? nil 'temp-var nil))))
(ert-deftest org-edna-condition-has-property ()
(let* ((pom-true (org-edna-find-test-heading org-edna-test-id-heading-four))
(pom-false (org-edna-find-test-heading org-edna-test-id-heading-one))
(block-true (org-with-point-at pom-true (org-get-heading)))
(block-false (org-with-point-at pom-false (org-get-heading))))
(org-edna-test-condition-form 'org-edna-condition/has-property?
pom-true pom-false
block-true block-false
"ID" org-edna-test-id-heading-four)))
(ert-deftest org-edna-condition-re-search ()
(pcase-let* ((case-fold-search nil)
(string "require")
(`(,pom-true ,block-true)
(with-current-buffer (find-file-noselect org-edna-tests-el)
(list (point-min-marker)
(format "Found %s in %s" string (buffer-name)))))
(`(,pom-false ,block-false)
(with-current-buffer (find-file-noselect org-edna-test-file)
(list (point-min-marker)
(format "Did Not Find %s in %s" string (buffer-name))))))
(org-edna-test-condition-form 'org-edna-condition/re-search?
pom-true pom-false
block-true block-false
string)))
(ert-deftest org-edna-condition/has-tags ()
(let* ((pom-true (org-edna-find-test-heading "0fa0d4dd-40f2-4251-a558-4c6e2898c2df"))
(pom-false (org-edna-find-test-heading org-edna-test-id-heading-one))
(block-true (org-with-point-at pom-true (org-get-heading)))
(block-false (org-with-point-at pom-false (org-get-heading))))
(org-edna-test-condition-form 'org-edna-condition/has-tags?
pom-true pom-false
block-true block-false
"test")))
(ert-deftest org-edna-condition/matches-tags ()
(org-edna-test-condition-form
'org-edna-condition/matches?
(org-edna-find-test-heading "0fa0d4dd-40f2-4251-a558-4c6e2898c2df")
(org-edna-find-test-heading org-edna-test-id-heading-one)
nil nil
"1&test")
(org-edna-test-condition-form
'org-edna-condition/matches?
(org-edna-find-test-heading org-edna-test-id-heading-four)
(org-edna-find-test-heading "0fa0d4dd-40f2-4251-a558-4c6e2898c2df")
nil nil
"TODO==\"DONE\""))
;;; Consideration
(ert-deftest org-edna-consideration/any ()
(let ((blocks-all-blocking `("a" "c" "b"))
(blocks-some-blocking `("a" nil "b"))
(blocks-no-blocking `(nil nil nil)))
(should (string-equal (org-edna-handle-consideration 'any blocks-all-blocking) "a"))
(should (string-equal (org-edna-handle-consideration 'any blocks-some-blocking) "a"))
(should (not (org-edna-handle-consideration 'any blocks-no-blocking)))))
(ert-deftest org-edna-consideration/all ()
(let ((blocks-all-blocking `("a" "c" "b"))
(blocks-some-blocking `(nil "c" nil))
(blocks-no-blocking `(nil nil nil)))
(should (string-equal (org-edna-handle-consideration 'all blocks-all-blocking) "a"))
(should (not (org-edna-handle-consideration 'all blocks-some-blocking)))
(should (not (org-edna-handle-consideration 'all blocks-no-blocking)))))
(ert-deftest org-edna-consideration/integer ()
(let ((blocks-all-blocking `("a" "c" "b"))
(blocks-some-blocking `("a" nil "b"))
(blocks-no-blocking `(nil nil nil)))
(should (string-equal (org-edna-handle-consideration 2 blocks-all-blocking) "a"))
(should (string-equal (org-edna-handle-consideration 2 blocks-some-blocking) "a"))
(should (not (org-edna-handle-consideration 2 blocks-no-blocking)))))
(ert-deftest org-edna-consideration/float ()
(let ((blocks-all-blocking `("a" "c" "b"))
(blocks-some-blocking `("a" nil "b"))
(blocks-no-blocking `(nil nil nil)))
(should (string-equal (org-edna-handle-consideration 0.25 blocks-all-blocking) "a"))
(should (string-equal (org-edna-handle-consideration 0.25 blocks-some-blocking) "a"))
(should (not (org-edna-handle-consideration 0.25 blocks-no-blocking)))))
;;; Full Run-through Tests from the Documentation
(defmacro org-edna-doc-test-setup (heading-id &rest body)
(declare (indent 1))
`(org-edna-with-test-heading ,heading-id
(save-restriction
;; Only allow operating on the current tree
(org-narrow-to-subtree)
;; Show the entire subtree
(outline-show-all)
,@body)))
(ert-deftest org-edna-doc-test/ancestors ()
(org-edna-doc-test-setup "24a0c3bb-7e69-4e9e-bb98-5aba2ff17bb1"
(pcase-let* ((`(,heading1-pom ,heading2-pom ,heading3-pom ,heading4-pom ,heading5-pom)
(org-edna-test-children-marks)))
;; Verify that we can't change the TODO state to DONE
(should (org-edna-test-check-block heading5-pom "Initial state of heading 5"))
;; Change the state at 4 to DONE
(org-edna-test-mark-done heading4-pom)
;; Verify that ALL ancestors need to be changed
(should (org-edna-test-check-block heading5-pom "Heading 5 after parent changed"))
(org-edna-test-mark-done heading1-pom heading3-pom)
;; Only need 1, 3, and 4 to change 5
(should (not (org-edna-test-check-block heading5-pom
"Heading 5 after all parents changed")))
;; Change the state back to TODO on all of them
(org-edna-test-mark-todo heading1-pom heading3-pom heading4-pom heading5-pom))))
(ert-deftest org-edna-doc-test/ancestors-cache ()
(let ((org-edna-finder-use-cache t))
(org-edna-doc-test-setup "24a0c3bb-7e69-4e9e-bb98-5aba2ff17bb1"
(pcase-let* ((`(,heading1-pom ,heading2-pom ,heading3-pom ,heading4-pom ,heading5-pom)
(org-edna-test-children-marks)))
;; Verify that we can't change the TODO state to DONE
(should (org-edna-test-check-block heading5-pom "Initial state of heading 5"))
;; Change the state at 4 to DONE
(org-edna-test-mark-done heading4-pom)
;; Verify that ALL ancestors need to be changed
(should (org-edna-test-check-block heading5-pom "Heading 5 after parent changed"))
(org-edna-test-mark-done heading1-pom heading3-pom)
;; Only need 1, 3, and 4 to change 5
(should (not (org-edna-test-check-block heading5-pom
"Heading 5 after all parents changed")))
;; Change the state back to TODO on all of them
(org-edna-test-mark-todo heading1-pom heading3-pom heading4-pom heading5-pom)))))
(ert-deftest org-edna-doc-test/descendants ()
(org-edna-doc-test-setup "cc18dc74-00e8-4081-b46f-e36800041fe7"
(pcase-let* ((`(,heading1-pom ,heading2-pom ,heading3-pom ,heading4-pom ,heading5-pom)
(org-edna-test-children-marks)))
(should (org-edna-test-check-block heading1-pom "Heading 1 initial state"))
;; Change the state at 2 to DONE
(org-edna-test-mark-done heading2-pom)
;; Verify that ALL descendants need to be changed
(should (org-edna-test-check-block heading1-pom "Heading 1 after changing 2"))
;; Try 3
(org-edna-test-mark-done heading3-pom)
;; Verify that ALL descendants need to be changed
(should (org-edna-test-check-block heading1-pom "Heading 1 after changing 3"))
;; Try 4
(org-edna-test-mark-done heading4-pom)
;; Verify that ALL descendants need to be changed
(should (org-edna-test-check-block heading1-pom "Heading 1 after changing 4"))
;; Try 5
(org-edna-test-mark-done heading5-pom)
;; Verify that ALL descendants need to be changed
(should (not (org-edna-test-check-block heading1-pom "Heading 1 after changing 5"))))))
(ert-deftest org-edna-doc-test/descendants-cache ()
(let ((org-edna-finder-use-cache t))
(org-edna-doc-test-setup "cc18dc74-00e8-4081-b46f-e36800041fe7"
(pcase-let* ((`(,heading1-pom ,heading2-pom ,heading3-pom ,heading4-pom ,heading5-pom)
(org-edna-test-children-marks)))
(should (org-edna-test-check-block heading1-pom "Heading 1 initial state"))
;; Change the state at 2 to DONE
(org-edna-test-mark-done heading2-pom)
;; Verify that ALL descendants need to be changed
(should (org-edna-test-check-block heading1-pom "Heading 1 after changing 2"))
;; Try 3
(org-edna-test-mark-done heading3-pom)
;; Verify that ALL descendants need to be changed
(should (org-edna-test-check-block heading1-pom "Heading 1 after changing 3"))
;; Try 4
(org-edna-test-mark-done heading4-pom)
;; Verify that ALL descendants need to be changed
(should (org-edna-test-check-block heading1-pom "Heading 1 after changing 4"))
;; Try 5
(org-edna-test-mark-done heading5-pom)
;; Verify that ALL descendants need to be changed
(should (not (org-edna-test-check-block heading1-pom "Heading 1 after changing 5")))))))
(ert-deftest org-edna-doc-test/laundry ()
"Test for the \"laundry\" example in the documentation."
(org-edna-doc-test-setup "e57ce099-9f37-47f4-a6bb-61a84eb1fbbe"
(pcase-let* ((`(,heading1-pom ,heading2-pom ,heading3-pom ,heading4-pom)
(org-edna-test-children-marks)))
;; Verify that headings 2, 3, and 4 are all blocked
(should (org-edna-test-check-block heading2-pom
"Initial attempt to change heading 2"))
(should (org-edna-test-check-block heading3-pom
"Initial attempt to change heading 3"))
(should (org-edna-test-check-block heading4-pom
"Initial attempt to change heading 4"))
;; Mark heading 1 as DONE
(should (not (org-edna-test-check-block heading1-pom
"Set heading 1 to DONE")))
;; Only heading 2 should have a scheduled time
(should (string-equal (org-entry-get heading2-pom "SCHEDULED")
"<2000-01-15 Sat 01:00>"))
(should (not (org-entry-get heading3-pom "SCHEDULED")))
(should (not (org-entry-get heading4-pom "SCHEDULED")))
;; The others should still be blocked.
(should (org-edna-test-check-block heading3-pom
"Second attempt to change heading 3"))
(should (org-edna-test-check-block heading4-pom
"Second attempt to change heading 4"))
;; Try changing heading 2
(should (not (org-edna-test-check-block heading2-pom
"Set heading 2 to DONE")))
(should (string-equal (org-entry-get heading3-pom "SCHEDULED")
"<2000-01-16 Sun 09:00>"))
;; 4 should still be blocked
(should (org-edna-test-check-block heading4-pom
"Second attempt to change heading 4")))))
(ert-deftest org-edna-doc-test/laundry-cache ()
"Test for the \"laundry\" example in the documentation.
This version enables cache, ensuring that the repeated calls to
the relative finders all still work while cache is enabled."
(let ((org-edna-finder-use-cache t))
(org-edna-doc-test-setup "e57ce099-9f37-47f4-a6bb-61a84eb1fbbe"
(pcase-let* ((`(,heading1-pom ,heading2-pom ,heading3-pom ,heading4-pom)
(org-edna-test-children-marks)))
;; Verify that headings 2, 3, and 4 are all blocked
(should (org-edna-test-check-block heading2-pom
"Initial attempt to change heading 2"))
(should (org-edna-test-check-block heading3-pom
"Initial attempt to change heading 3"))
(should (org-edna-test-check-block heading4-pom
"Initial attempt to change heading 4"))
;; Mark heading 1 as DONE
(should (not (org-edna-test-check-block heading1-pom
"Set heading 1 to DONE")))
;; Only heading 2 should have a scheduled time
(should (string-equal (org-entry-get heading2-pom "SCHEDULED")
"<2000-01-15 Sat 01:00>"))
(should (not (org-entry-get heading3-pom "SCHEDULED")))
(should (not (org-entry-get heading4-pom "SCHEDULED")))
;; The others should still be blocked.
(should (org-edna-test-check-block heading3-pom
"Second attempt to change heading 3"))
(should (org-edna-test-check-block heading4-pom
"Second attempt to change heading 4"))
;; Try changing heading 2
(should (not (org-edna-test-check-block heading2-pom
"Set heading 2 to DONE")))
(should (string-equal (org-entry-get heading3-pom "SCHEDULED")
"<2000-01-16 Sun 09:00>"))
;; 4 should still be blocked
(should (org-edna-test-check-block heading4-pom
"Second attempt to change heading 4"))))))
(ert-deftest org-edna-doc-test/nightly ()
(org-edna-doc-test-setup "8b6d9820-d943-4622-85c9-4a346e033453"
(pcase-let* ((`(,nightly-pom ,lunch-pom ,door-pom ,dog-pom)
(org-edna-test-children-marks)))
;; Verify that Nightly is blocked
(should (org-edna-test-check-block nightly-pom "Initial Nightly Check"))
;; Check off Lunch, and verify that nightly is still blocked
(org-edna-test-mark-done lunch-pom)
(should (org-edna-test-check-block nightly-pom "Nightly after Lunch"))
;; Check off Door, and verify that nightly is still blocked
(org-edna-test-mark-done door-pom)
(should (org-edna-test-check-block nightly-pom "Nightly after Door"))
;; Check off Dog. This should trigger the others.
(org-edna-test-mark-done dog-pom)
(should (org-edna-test-compare-todos lunch-pom "TODO" "Lunch after Nightly Trigger"))
(should (org-edna-test-compare-todos door-pom "TODO" "Door after Nightly Trigger"))
(should (org-edna-test-compare-todos dog-pom "TODO" "Dog after Nightly Trigger"))
(should (string-equal (org-entry-get nightly-pom "DEADLINE")
"<2000-01-16 Sun +1d>")))))
(ert-deftest org-edna-doc-test/nightly-cache ()
(let ((org-edna-finder-use-cache t))
(org-edna-doc-test-setup "8b6d9820-d943-4622-85c9-4a346e033453"
(pcase-let* ((`(,nightly-pom ,lunch-pom ,door-pom ,dog-pom)
(org-edna-test-children-marks)))
;; Verify that Nightly is blocked
(should (org-edna-test-check-block nightly-pom "Initial Nightly Check"))
;; Check off Lunch, and verify that nightly is still blocked
(org-edna-test-mark-done lunch-pom)
(should (org-edna-test-check-block nightly-pom "Nightly after Lunch"))
;; Check off Door, and verify that nightly is still blocked
(org-edna-test-mark-done door-pom)
(should (org-edna-test-check-block nightly-pom "Nightly after Door"))
;; Check off Dog. This should trigger the others.
(org-edna-test-mark-done dog-pom)
(should (org-edna-test-compare-todos lunch-pom "TODO" "Lunch after Nightly Trigger"))
(should (org-edna-test-compare-todos door-pom "TODO" "Door after Nightly Trigger"))
(should (org-edna-test-compare-todos dog-pom "TODO" "Dog after Nightly Trigger"))
(should (string-equal (org-entry-get nightly-pom "DEADLINE")
"<2000-01-16 Sun +1d>"))))))
(ert-deftest org-edna-doc-test/daily ()
(org-edna-doc-test-setup "630805bb-a864-4cdc-9a6f-0f126e887c66"
(pcase-let* ((`(,daily-pom ,lunch-pom ,door-pom ,dog-pom)
(org-edna-test-children-marks)))
;; Check off Lunch. This should trigger the others.
(org-edna-test-mark-done lunch-pom)
(should (org-edna-test-compare-todos lunch-pom "TODO" "Lunch after Daily Trigger"))
(should (org-edna-test-compare-todos door-pom "TODO" "Door after Daily Trigger"))
(should (org-edna-test-compare-todos dog-pom "TODO" "Dog after Daily Trigger"))
(should (string-equal (org-entry-get daily-pom "DEADLINE")
"<2000-01-16 Sun +1d>"))
;; Check off Door. This should trigger the others.
(org-edna-test-mark-done door-pom)
(should (org-edna-test-compare-todos lunch-pom "TODO" "Lunch after Door Trigger"))
(should (org-edna-test-compare-todos door-pom "TODO" "Door after Door Trigger"))
(should (org-edna-test-compare-todos dog-pom "TODO" "Dog after Door Trigger"))
(should (string-equal (org-entry-get daily-pom "DEADLINE")
"<2000-01-17 Mon +1d>"))
;; Check off Dog. This should trigger the others.
(org-edna-test-mark-done dog-pom)
(should (org-edna-test-compare-todos lunch-pom "TODO" "Lunch after Dog Trigger"))
(should (org-edna-test-compare-todos door-pom "TODO" "Door after Dog Trigger"))
(should (org-edna-test-compare-todos dog-pom "TODO" "Dog after Dog Trigger"))
(should (string-equal (org-entry-get daily-pom "DEADLINE")
"<2000-01-18 Tue +1d>")))))
(ert-deftest org-edna-doc-test/weekly ()
(org-edna-doc-test-setup "cf529a5e-1b0c-40c3-8f85-fe2fc4df0ffd"
(pcase-let* ((`(,weekly-pom ,lunch-pom ,door-pom ,dog-pom)
(org-edna-test-children-marks)))
;; Check off Lunch. This should trigger the others.
(org-edna-test-mark-done lunch-pom)
(should (org-edna-test-compare-todos lunch-pom "TODO" "Lunch after Weekly Trigger"))
(should (org-edna-test-compare-todos door-pom "TODO" "Door after Weekly Trigger"))
(should (org-edna-test-compare-todos dog-pom "TODO" "Dog after Weekly Trigger"))
(should (string-equal (org-entry-get weekly-pom "DEADLINE")
"<2000-01-16 Sun +1d>")))))
(ert-deftest org-edna-doc-test/basic-shower ()
(org-edna-doc-test-setup "34d67756-927b-4a21-a62d-7989bd138946"
(pcase-let* ((`(,shower-pom ,towels-pom) (org-edna-test-children-marks)))
;; Verify towels is blocked
(should (org-edna-test-check-block towels-pom "Initial Towels Check"))
;; Check off "Take Shower" and verify that it incremented the property
(org-edna-test-mark-done shower-pom)
(should (string-equal (org-entry-get shower-pom "COUNT") "1"))
;; Verify towels is blocked
(should (org-edna-test-check-block towels-pom "Towels Check, Count=1"))
;; Check off "Take Shower" and verify that it incremented the property
(org-edna-test-mark-done shower-pom)
(should (string-equal (org-entry-get shower-pom "COUNT") "2"))
;; Verify towels is blocked
(should (org-edna-test-check-block towels-pom "Towels Check, Count=2"))
;; Check off "Take Shower" and verify that it incremented the property
(org-edna-test-mark-done shower-pom)
(should (string-equal (org-entry-get shower-pom "COUNT") "3"))
;; Verify that towels is no longer blocked.
(should (not (org-edna-test-check-block towels-pom "Towels Check, Count=3")))
;; Verify that the property was reset.
(should (string-equal (org-entry-get shower-pom "COUNT") "0")))))
(ert-deftest org-edna-doc-test/snow-shoveling ()
(org-edna-doc-test-setup "b1d89bd8-db96-486e-874c-98e2b3a8cbf2"
(pcase-let* ((`(,monday-pom ,tuesday-pom ,wednesday-pom ,shovel-pom)
(org-edna-test-children-marks)))
;; Verify shovels is blocked
(should (org-edna-test-check-block shovel-pom "Initial Shovel Check"))
;; Mark Monday as done
(org-edna-test-mark-done monday-pom)
(should (not (org-edna-test-check-block shovel-pom "Shovel after changing Monday")))
;; Reset
(org-edna-test-mark-todo monday-pom tuesday-pom wednesday-pom shovel-pom)
;; Mark Tuesday as done
(org-edna-test-mark-done tuesday-pom)
(should (not (org-edna-test-check-block shovel-pom "Shovel after changing Tuesday")))
;; Reset
(org-edna-test-mark-todo monday-pom tuesday-pom wednesday-pom shovel-pom)
;; Mark Wednesday as done
(org-edna-test-mark-done wednesday-pom)
(should (not (org-edna-test-check-block shovel-pom "Shovel after changing Wednesday"))))))
(ert-deftest org-edna-doc-test/consider-fraction ()
(org-edna-doc-test-setup "7de5af8b-a226-463f-8360-edd88b99462a"
(pcase-let* ((`(,shovel-pom ,room-pom ,vacuum-pom ,lunch-pom ,edna-pom)
(org-edna-test-children-marks)))
;; Verify Edna is blocked
(should (org-edna-test-check-block edna-pom "Initial Edna Check"))
;; Mark Shovel snow as done
(org-edna-test-mark-done shovel-pom)
;; Verify Edna is still blocked
(should (org-edna-test-check-block edna-pom "Edna Check after Shovel"))
;; Mark Vacuum as done
(org-edna-test-mark-done vacuum-pom)
;; Verify Edna is still blocked
(should (org-edna-test-check-block edna-pom "Edna Check after Vacuum"))
;; Mark Room as done
(org-edna-test-mark-done room-pom)
;; Verify Edna is no longer blocked
(should (not (org-edna-test-check-block edna-pom "Edna Check after Room"))))))
(ert-deftest org-edna-doc-test/consider-number ()
(org-edna-doc-test-setup "b79279f7-be3c-45ac-96dc-6e962a5873d4"
(pcase-let* ((`(,shovel-pom ,room-pom ,vacuum-pom ,lunch-pom ,edna-pom)
(org-edna-test-children-marks)))
;; Verify Edna is blocked
(should (org-edna-test-check-block edna-pom "Initial Edna Check"))
;; Mark Shovel snow as done
(org-edna-test-mark-done shovel-pom)
;; Verify Edna is still blocked
(should (org-edna-test-check-block edna-pom "Edna Check after Shovel"))
;; Mark Vacuum as done
(org-edna-test-mark-done vacuum-pom)
;; Verify Edna is still blocked
(should (org-edna-test-check-block edna-pom "Edna Check after Vacuum"))
;; Mark Room as done
(org-edna-test-mark-done room-pom)
;; Verify Edna is no longer blocked
(should (not (org-edna-test-check-block edna-pom "Edna Check after Room"))))))
(ert-deftest org-edna-doc-test/has-tags ()
(org-edna-doc-test-setup "6885e932-2c3e-4f20-ac22-5f5a0e791d67"
(pcase-let* ((`(,first-pom ,second-pom ,third-pom)
(org-edna-test-children-marks)))
;; Verify that 3 is blocked
(should (org-edna-test-check-block third-pom "Initial Check"))
;; Remove the tag from Task 1
(org-with-point-at first-pom
(org-set-tags-to ""))
;; Verify that 3 is still blocked
(should (org-edna-test-check-block third-pom "Check after removing tag1"))
;; Remove the tag from Task 2
(org-with-point-at second-pom
(org-set-tags-to ""))
;; Verify that 3 is no longer blocked
(should (not (org-edna-test-check-block third-pom "Check after removing tag2"))))))
(ert-deftest org-edna-doc-test/matches ()
(org-edna-doc-test-setup "8170bf82-c2ea-49e8-bd79-97a95176783f"
(pcase-let* ((`(,first-pom ,second-pom ,third-pom) (org-edna-test-children-marks)))
;; Verify that 3 is blocked
(should (org-edna-test-check-block third-pom "Initial Check"))
;; Set 1 to DONE
(org-edna-test-mark-done first-pom)
;; Verify that 3 is still blocked
(should (org-edna-test-check-block third-pom "Check after First"))
;; Set 2 to DONE
(org-edna-test-mark-done second-pom)
;; Verify that 3 is no longer blocked
(should (not (org-edna-test-check-block third-pom "Check after Second"))))))
(ert-deftest org-edna-doc-test/chain ()
(org-edna-doc-test-setup "1bd282ea-9238-47ea-9b4d-dafba19d278b"
(pcase-let* ((`(,first-pom ,second-pom) (org-edna-test-children-marks)))
;; Set 1 to DONE
(org-edna-test-mark-done first-pom)
(should (string-equal (org-entry-get second-pom "COUNT") "2")))))
(ert-deftest org-edna-doc-test/multiple-blockers ()
(org-edna-doc-test-setup "61e754c2-f292-42b5-8166-e4298dc190d6"
(pcase-let* ((`(,first-pom ,second-pom ,third-pom) (org-edna-test-children-marks)))
;; Verify that 3 is blocked
(should (org-edna-test-check-block third-pom "Initial Check"))
;; Set 1 to DONE
(org-edna-test-mark-done first-pom)
;; Verify that 3 is still blocked
(should (org-edna-test-check-block third-pom "Check after First"))
;; Reset 1
(org-edna-test-mark-todo first-pom)
;; Set 2 to DONE
(org-edna-test-mark-done second-pom)
;; Verify that 3 is still blocked
(should (org-edna-test-check-block third-pom "Check after Second"))
;; Set 1 to DONE
(org-edna-test-mark-done first-pom)
;; Verify that 3 is no longer blocked.
(should (not (org-edna-test-check-block third-pom "Check after Both"))))))
(ert-deftest org-edna-user-test/time-spec ()
(org-edna-doc-test-setup "5b63293c-23ef-40e7-ad8e-093e4c1e1464"
(pcase-let* ((`(,first-pom ,second-pom ,third-pom) (org-edna-test-children-marks)))
(org-edna-test-mark-done first-pom)
;; Test time is 2000-01-15, so this should be a week later
(should (string-equal (org-entry-get second-pom "SCHEDULED")
"<2000-01-22 Sat>")))))
(provide 'org-edna-tests)
;;; org-edna-tests.el ends here
org-edna-1.0.2/org-edna.org 0000644 0001752 0001753 00000137446 13425536050 014066 0 ustar elpa elpa #+TITLE: Org Edna
#+AUTHOR: Ian Dunn
#+EMAIL: dunni@gnu.org
#+DATE: {{{modification-time}}}
#+STARTUP: overview
#+STARTUP: indent
#+TODO: FIXME | FIXED
#+OPTIONS: toc:2 num:nil timestamp:nil \n:nil |:t ':t email:t H:4
#+OPTIONS: *:t <:t d:nil todo:nil pri:nil tags:not-in-toc -:nil
#+TEXINFO_DIR_CATEGORY: Emacs
#+TEXINFO_DIR_TITLE: Org Edna: (org-edna)
#+TEXINFO_DIR_DESC: Extensible Dependencies 'N' Actions for Org Mode tasks
* Copying
Copyright (C) 2017-2018 Free Software Foundation, Inc.
#+BEGIN_QUOTE
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
#+END_QUOTE
* Introduction
:PROPERTIES:
:CUSTOM_ID: introduction
:DESCRIPTION: A Brief Introduction to Edna
:END:
Extensible Dependencies 'N' Actions (EDNA) for Org Mode tasks
Edna provides an extensible means of specifying conditions which must be
fulfilled before a task can be completed and actions to take once it is.
Org Edna runs when either the BLOCKER or TRIGGER properties are set on a
heading, and when it is changing from a TODO state to a DONE state.
For brevity, we use TODO state to indicate any state in ~org-not-done-keywords~,
and DONE state to indicate any state in ~org-done-keywords~.
** Installation and Setup
:PROPERTIES:
:DESCRIPTION: How to install Edna
:END:
*Requirements*
| Emacs | 25.1 |
| seq | 2.19 |
| org | 9.0.5 |
There are two ways to install Edna: From GNU ELPA, or from source.
From ELPA:
#+BEGIN_EXAMPLE
M-x package-install org-edna
#+END_EXAMPLE
From Source:
#+BEGIN_SRC shell
bzr branch https://bzr.savannah.gnu.org/r/org-edna-el/ org-edna
#+END_SRC
After that, add the following to your init file (typically .emacs):
#+BEGIN_SRC emacs-lisp
;; Only necessary if installing from source
(add-to-list 'load-path "/full/path/to/org-edna/")
(require 'org-edna)
;; Always necessary
(org-edna-load)
#+END_SRC
If you ever want to disable Edna, run ~org-edna-unload~.
** Basic Operation
:PROPERTIES:
:CUSTOM_ID: operation
:DESCRIPTION: How to use Edna
:END:
Let's start with an example: Say you want to do laundry, but once you've put
your clothes in the washer, you forget about it. Even with a tool like
org-notify or appt, Org won't know when to remind you. If you've got them
scheduled for an hour after the other, maybe you forgot one time, or ran a
little late. Now Org will remind you too early.
Edna can handle this for you like so:
#+BEGIN_SRC org
,* TODO Put clothes in washer
SCHEDULED: <2017-04-08 Sat 09:00>
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:END:
,* TODO Put clothes in dryer
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
,* TODO Fold laundry
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
,* TODO Put clothes away
:PROPERTIES:
:TRIGGER: next-sibling scheduled!("++1h")
:BLOCKER: previous-sibling
:END:
#+END_SRC
After you've put your clothes in the washer and mark the task DONE, Edna will
schedule the following task for one hour after you set the first heading as
done.
Another example might be a checklist that you've done so many times that you do
part of it on autopilot:
#+BEGIN_SRC org
,* TODO Address all TODOs in code
,* TODO Commit Code to Repository
#+END_SRC
The last thing anyone wants is to find out that some part of the code on which
they've been working for days has a surprise waiting for them. Once again, Edna
can help:
#+BEGIN_SRC org
,* TODO Address all TODOs in code
:PROPERTIES:
:BLOCKER: file("main.cpp") file("code.cpp") re-search?("TODO")
:END:
,* TODO Commit Code to Repository
#+END_SRC
** Blockers
:PROPERTIES:
:CUSTOM_ID: blockers
:DESCRIPTION: Blocking a TODO Item
:END:
A blocker indicates conditions which must be met in order for a heading to be
marked as DONE. Typically, this will be a list of headings that must be marked
as DONE.
** Triggers
:PROPERTIES:
:CUSTOM_ID: triggers
:DESCRIPTION: Triggering actions after completing a task
:END:
A trigger is an action to take when a heading is set to done. For example,
scheduling another task, marking another task as TODO, or renaming a file.
** Syntax
:PROPERTIES:
:CUSTOM_ID: syntax
:DESCRIPTION: Basic explanation of Edna's syntax
:END:
#+cindex: syntax
Edna has its own language for commands, the basic form of which is KEYWORD(ARG1 ARG2 ...)
KEYWORD can be any valid lisp symbol, such as key-word, KEY_WORD!, or keyword?.
Each argument can be one of the following:
- A symbol, such as arg or org-mode
- A quoted string, such as "hello" or "My name is Edna"
- A number, such as 0.5, +1e3, or -5
- A UUID, such as c5e30c76-879a-494d-9281-3a4b559c1a3c
Each argument takes specific datatypes as input, so be sure to read the entry
before using it.
The parentheses can be omitted for commands with no arguments.
* Basic Features
:PROPERTIES:
:CUSTOM_ID: basic
:DESCRIPTION: Finders and Actions
:END:
The most basic features of Edna are *finders* and *actions*.
** Finders
:PROPERTIES:
:DESCRIPTION: How to find targets
:CUSTOM_ID: finders
:END:
A finder specifies locations from which to test conditions or perform actions.
These locations are referred to as "targets". The current heading, i.e. the one
that is being blocked or triggered, is referred to as the "source" heading.
More than one finder may be used. In this case, the targets are merged
together, removing any duplicates.
Many finders take additional options, marked "OPTIONS". See [[#relatives][relatives]] for
information on these options.
*** ancestors
:PROPERTIES:
:DESCRIPTION: Find a list of ancestors
:CUSTOM_ID: ancestors
:END:
- Syntax: ancestors(OPTIONS...)
The ~ancestors~ finder returns a list of the source heading's ancestors.
For example:
#+BEGIN_SRC org
,* TODO Heading 1
,** TODO Heading 2
,** TODO Heading 3
,*** TODO Heading 4
,**** TODO Heading 5
:PROPERTIES:
:BLOCKER: ancestors
:END:
#+END_SRC
In the above example, "Heading 5" will be blocked until "Heading 1", "Heading
3", and "Heading 4" are marked "DONE", while "Heading 2" is ignored.
*** children
:PROPERTIES:
:CUSTOM_ID: children
:DESCRIPTION: Find all immediate children
:END:
- Syntax: children(OPTIONS...)
The ~children~ finder returns a list of the *immediate* children of the source
heading. If the source has no children, no target is returned.
In order to get all levels of children of the source heading, use the
[[#descendants][descendants]] keyword instead.
*** descendants
:PROPERTIES:
:CUSTOM_ID: descendants
:DESCRIPTION: Find all descendants
:END:
- Syntax: descendants(OPTIONS...)
The ~descendants~ finder returns a list of all descendants of the source heading.
#+BEGIN_SRC org
,* TODO Heading 1
:PROPERTIES:
:BLOCKER: descendants
:END:
,** TODO Heading 2
,*** TODO Heading 3
,**** TODO Heading 4
,***** TODO Heading 5
#+END_SRC
In the above example, "Heading 1" will block until Headings 2, 3, 4, and 5 are
DONE.
*** file
:PROPERTIES:
:CUSTOM_ID: file
:DESCRIPTION: Find a file by name
:END:
- Syntax: file("FILE")
The ~file~ finder finds a single file, specified as a string. The returned target
will be the minimum point in the file.
Note that this does not give a valid heading, so any conditions
or actions that require will throw an error. Consult the
documentation for individual actions or conditions to determine
which ones will and won't work.
See [[#conditions][conditions]] for how to set a different condition. For example:
#+BEGIN_SRC org
,* TODO Test
:PROPERTIES:
:BLOCKER: file("~/myfile.org") headings?
:END:
#+END_SRC
Here, "Test" will block until myfile.org is clear of headings.
*** first-child
:PROPERTIES:
:CUSTOM_ID: first-child
:DESCRIPTION: Find the first child of a heading
:END:
- Syntax: first-child(OPTIONS...)
Return the first child of the source heading. If the source heading has no
children, no target is returned.
*** ids
:PROPERTIES:
:DESCRIPTION: Find a list of headings with given IDs
:CUSTOM_ID: ids
:END:
- Syntax: id(ID1 ID2 ...)
The ~ids~ finder will search for headings with given IDs, using ~org-id~. Any
number of UUIDs may be specified. For example:
#+BEGIN_SRC org
,* TODO Test
:PROPERTIES:
:BLOCKER: ids(62209a9a-c63b-45ef-b8a8-12e47a9ceed9 6dbd7921-a25c-4e20-b035-365677e00f30)
:END:
#+END_SRC
Here, "Test" will block until the heading with ID
62209a9a-c63b-45ef-b8a8-12e47a9ceed9 and the heading with ID
6dbd7921-a25c-4e20-b035-365677e00f30 are set to "DONE".
Note that UUIDs need not be quoted; Edna will handle that for you.
*** match
:PROPERTIES:
:CUSTOM_ID: match
:DESCRIPTION: Good old tag matching
:END:
- Syntax: match("MATCH-STRING" SCOPE SKIP)
The ~match~ keyword will take any arguments that ~org-map-entries~ usually takes.
In fact, the arguments to ~match~ are passed straight into ~org-map-entries~.
#+BEGIN_SRC org
,* TODO Test
:PROPERTIES:
:BLOCKER: match("test&mine" agenda)
:END:
#+END_SRC
"Test" will block until all entries tagged "test" and "mine" in the agenda files
are marked DONE.
See the documentation for ~org-map-entries~ for a full explanation of the first
argument.
*** next-sibling
:PROPERTIES:
:CUSTOM_ID: next-sibling
:DESCRIPTION: Find the next sibling
:END:
- Syntax: next-sibling(OPTIONS...)
The ~next-sibling~ keyword returns the next sibling of the source heading, if any.
*** next-sibling-wrap
:PROPERTIES:
:CUSTOM_ID: next-sibling-wrap
:DESCRIPTION: Find the next sibling, wrapping around
:END:
- Syntax: next-sibling-wrap(OPTIONS...)
Find the next sibling of the source heading, if any. If there isn't, wrap back
around to the first heading in the same subtree.
*** olp
:PROPERTIES:
:CUSTOM_ID: olp
:DESCRIPTION: Find a heading by its outline path
:END:
- Syntax: olp("FILE" "OLP")
Finds the heading given by OLP in FILE. Both arguments are strings.
#+BEGIN_SRC org
,* TODO Test
:PROPERTIES:
:BLOCKER: olp("test.org" "path/to/heading")
:END:
#+END_SRC
"Test" will block if the heading "path/to/heading" in "test.org" is not DONE.
*** org-file
:PROPERTIES:
:CUSTOM_ID: org-file
:DESCRIPTION: Find a file in org-directory
:END:
- Syntax: org-file("FILE")
A special form of ~file~, ~org-file~ will find FILE in ~org-directory~.
FILE is the relative path of a file in ~org-directory~. Nested
files are allowed, such as "my-directory/my-file.org". The
returned target is the minimum point of FILE.
#+BEGIN_SRC org
,* TODO Test
:PROPERTIES:
:BLOCKER: org-file("test.org")
:END:
#+END_SRC
Note that the file still requires an extension; the "org" here
just means to look in ~org-directory~, not necessarily an
Org mode file.
*** parent
:PROPERTIES:
:CUSTOM_ID: parent
:DESCRIPTION: Find a parent
:END:
- Syntax: parent(OPTIONS...)
Returns the parent of the source heading, if any.
*** previous-sibling
:PROPERTIES:
:CUSTOM_ID: previous-sibling
:DESCRIPTION: Find the previous sibling
:END:
- Syntax: previous-sibling(OPTIONS...)
Returns the previous sibling of the source heading on the same level.
*** previous-sibling-wrap
:PROPERTIES:
:CUSTOM_ID: previous-sibling-wrap
:DESCRIPTION: Find the previous sibling, with wrapping
:END:
- Syntax: previous-sibling-wrap(OPTIONS...)
Returns the previous sibling of the source heading on the same level.
*** relatives
:PROPERTIES:
:CUSTOM_ID: relatives
:DESCRIPTION: Generic relative finder
:END:
Find some relative of the current heading.
- Syntax: relatives(OPTION OPTION...)
- Syntax: chain-find(OPTION OPTION...)
Identical to the chain argument in org-depend, relatives selects its single
target using the following method:
1. Creates a list of possible targets
2. Filters the targets from Step 1
3. Sorts the targets from Step 2
One option from each of the following three categories may be
used; if more than one is specified, the last will be used.
Filtering is the exception to this; each filter argument adds to
the current filter. Apart from that, argument order is
irrelevant.
The chain-find finder is also provided for backwards
compatibility, and for similarity to org-depend.
All arguments are symbols, unless noted otherwise.
*Selection*
- from-top: Select siblings of the current heading, starting at the top
- from-bottom: As above, but from the bottom
- from-current: Selects siblings, starting from the heading (wraps)
- no-wrap: As above, but without wrapping
- forward-no-wrap: Find entries on the same level, going forward
- forward-wrap: As above, but wrap when the end is reached
- backward-no-wrap: Find entries on the same level, going backward
- backward-wrap: As above, but wrap when the start is reached
- walk-up: Walk up the tree, excluding self
- walk-up-with-self: As above, but including self
- walk-down: Recursively walk down the tree, excluding self
- walk-down-with-self: As above, but including self
- step-down: Collect headings from one level down
*Filtering*
- todo-only: Select only targets with TODO state set that isn't a DONE state
- todo-and-done-only: Select all targets with a TODO state set
- no-comments: Skip commented headings
- no-archive: Skip archived headings
- NUMBER: Only use that many headings, starting from the first one
If passed 0, use all headings
If <0, omit that many headings from the end
- "+tag": Only select headings with given tag
- "-tag": Only select headings without tag
- "REGEX": select headings whose titles match REGEX
*Sorting*
- no-sort: Remove other sorting in affect
- reverse-sort: Reverse other sorts (stacks with other sort methods)
- random-sort: Sort in a random order
- priority-up: Sort by priority, highest first
- priority-down: Same, but lowest first
- effort-up: Sort by effort, highest first
- effort-down: Sort by effort, lowest first
- scheduled-up: Scheduled time, farthest first
- scheduled-down: Scheduled time, closest first
- deadline-up: Deadline time, farthest first
- deadline-down: Deadline time, closest first
- timestamp-up: Timestamp time, farthest first
- timestamp-down: Timestamp time, closest first
Many of the other finders are shorthand for argument combinations of relative:
- [[#ancestors][ancestors]] :: walk-up
- [[#children][children]] :: step-down
- [[#descendants][descendants]] :: walk-down
- [[#first-child][first-child]] :: step-down 1
- [[#next-sibling][next-sibling]] :: forward-no-wrap 1
- [[#next-sibling-wrap][next-sibling-wrap]] :: forward-wrap 1
- [[#parent][parent]] :: walk-up 1
- [[#previous-sibling][previous-sibling]] :: backward-no-wrap 1
- [[#previous-sibling-wrap][previous-sibling-wrap]] :: backward-wrap 1
- [[#rest-of-siblings][rest-of-siblings]] :: forward-no-wrap
- [[#rest-of-siblings-wrap][rest-of-siblings-wrap]] :: forward-wrap
- [[#siblings][siblings]] :: from-top
- [[#siblings-wrap][siblings-wrap]] :: forward-wrap
Because these are implemented as shorthand, any arguments for relatives may also
be passed to one of these finders.
*** rest-of-siblings
:PROPERTIES:
:CUSTOM_ID: rest-of-siblings
:DESCRIPTION: Find the remaining siblings
:END:
- Syntax: rest-of-siblings(OPTIONS...)
Starting from the heading following the current one, all same-level siblings
are returned.
*** rest-of-siblings-wrap
:PROPERTIES:
:CUSTOM_ID: rest-of-siblings-wrap
:DESCRIPTION: Find the remaining siblings, with wrapping
:END:
- Syntax: rest-of-siblings-wrap(OPTIONS...)
Starting from the heading following the current one, all same-level siblings
are returned. When the end is reached, wrap back to the beginning.
*** self
:PROPERTIES:
:CUSTOM_ID: self
:END:
- Syntax: self
Returns the source heading.
*** siblings
:PROPERTIES:
:CUSTOM_ID: siblings
:DESCRIPTION: Find all the siblings
:END:
- Syntax: siblings(OPTIONS...)
Returns all siblings of the source heading as targets, starting from the first
sibling.
*** siblings-wrap
:PROPERTIES:
:CUSTOM_ID: siblings-wrap
:DESCRIPTION: Find the siblings, but wrap around
:END:
- Syntax: siblings-wrap(OPTIONS...)
Finds the siblings on the same level as the source heading, wrapping when it
reaches the end.
Identical to the [[#rest-of-siblings-wrap][rest-of-siblings-wrap]] finder.
** Actions
:PROPERTIES:
:DESCRIPTION: Next steps
:END:
Once Edna has collected its targets for a trigger, it will perform actions on
them.
Actions must always end with '!'.
*** Scheduled/Deadline
:PROPERTIES:
:CUSTOM_ID: planning
:DESCRIPTION: Operate on planning information
:END:
- Syntax: scheduled!(OPTIONS)
- Syntax: deadline!(OPTIONS)
Set the scheduled or deadline time of any target headings.
There are several forms that the planning keywords can take. In the following,
PLANNING is either scheduled or deadline.
- PLANNING!("DATE[ TIME]")
Sets PLANNING to DATE at TIME. If DATE is a weekday instead of a date, then
set PLANNING to the following weekday. If TIME is not specified, only a date
will be added to the target.
Any string recognized by ~org-read-date~ may be used for DATE.
TIME is a time string, such as HH:MM.
- PLANNING!(rm|remove)
Remove PLANNING from all targets. The argument to this form may be either a
string or a symbol.
- PLANNING!(copy|cp)
Copy PLANNING info verbatim from the source heading to all targets. The
argument to this form may be either a string or a symbol.
- PLANNING!("[+|-|++|--]NTHING[ [+|-]LANDING]")
Increment(+) or decrement(-) target's PLANNING by N THINGs relative to either
itself (+/-) or the current time (++/--).
N is an integer
THING is one of y (years), m (months), d (days), h (hours), M (minutes), a
(case-insensitive) day of the week or its abbreviation, or the strings
"weekday" or "wkdy".
If a day of the week is given as THING, move forward or backward N weeks to
find that day of the week.
If one of "weekday" or "wkdy" is given as THING, move forward or backward N
days, moving forward or backward to the next weekday.
This form may also include a "landing" specifier to control where in the week
the final date lands. LANDING may be one of the following:
- A day of the week, which means adjust the final date forward (+) or backward
(-) to land on that day of the week.
- One of "weekday" or "wkdy", which means adjust the target date to the
closest weekday.
- One of "weekend" or "wknd", which means adjust the target date to the
closest weekend.
- PLANNING!("float [+|-|++|--]N DAYNAME[ MONTH[ DAY]]")
Set time to the date of the Nth DAYNAME before/after MONTH DAY, as per
~diary-float~.
N is an integer.
DAYNAME may be either an integer, where 0=Sunday, 1=Monday, etc., or a string
for that day.
MONTH may be an integer, 1-12, or a month's string. If MONTH is empty, the
following (+) or previous (-) month relative to the target's time (+/-) or the
current time (++/--).
DAY is an integer, or empty or 0 to use the first of the month (+) or the last
of the month (-).
Examples:
- scheduled!("Mon 09:00") :: Set SCHEDULED to the following Monday at 9:00
- deadline!("++2h") :: Set DEADLINE to two hours from now.
- deadline!(copy) deadline!("+1h") :: Copy the source deadline to the target, then increment it by an hour.
- scheduled!("+1wkdy") :: Set SCHEDULED to the next weekday
- scheduled!("+1d +wkdy") :: Same as above
- deadline!("+1m -wkdy") :: Set DEADLINE up one month, but move backward to find a weekend
- scheduled!("float 2 Tue Feb") :: Set SCHEDULED to the second Tuesday in the following February
- scheduled!("float 3 Thu") :: Set SCHEDULED to the third Thursday in the following month
**** Timestamp Format
:PROPERTIES:
:CUSTOM_ID: ts_format
:DESCRIPTION: Specifying a timestamp format
:END:
When using one of the planning modifiers, it isn't always possible to deduce how
the timestamp format will be chosen if using ++ or --. The following method is
used:
1. If the target heading already has a timestamp, that format is used.
2. If the modifier with the ++ or -- is "h" or "M" (hours or minutes), long
format (includes time) is used.
3. If the property ~EDNA_TS_FORMAT~ is set on the target heading, its value will
be used. It should be either ~long~ for long format (includes time) or
~short~ for short format (does not include time).
4. The user variable ~org-edna-timestamp-format~ is the final fallback. It
should be either the symbol ~long~ or ~short~. It defaults to ~short~.
*** TODO State
:PROPERTIES:
:CUSTOM_ID: todo!
:DESCRIPTION: Set todo state
:END:
- Syntax: todo!(NEW-STATE)
Sets the TODO state of the target heading to NEW-STATE.
NEW-STATE may either be a string or a symbol denoting the new TODO state. It
can also be the empty string, in which case the TODO state is removed.
Example:
#+BEGIN_SRC org
,* TODO Heading 1
:PROPERTIES:
:TRIGGER: next-sibling todo!(DONE)
:END:
,* TODO Heading 2
#+END_SRC
In this example, when "Heading 1" is marked as DONE, it will also mark "Heading
2" as DONE:
#+BEGIN_SRC org
,* DONE Heading 1
:PROPERTIES:
:TRIGGER: next-sibling todo!(DONE)
:END:
,* DONE Heading 2
#+END_SRC
*** Archive
:PROPERTIES:
:CUSTOM_ID: archive!
:DESCRIPTION: Archive targets
:END:
- Syntax: archive!
Archives all targets with confirmation.
Confirmation is controlled with ~org-edna-prompt-for-archive~. If this option is
nil, Edna will not ask before archiving targets.
*** Chain Property
:PROPERTIES:
:CUSTOM_ID: chain!
:DESCRIPTION: Copy properties from source to targets
:END:
- Syntax: chain!("PROPERTY")
Copies PROPERTY from the source entry to all targets. Does nothing if the
source heading has no property PROPERTY.
Example:
#+BEGIN_SRC org
,* TODO Heading 1
:PROPERTIES:
:COUNTER: 2
:TRIGGER: next-sibling chain!("COUNTER")
:END:
,* TODO Heading 2
#+END_SRC
In this example, when "Heading 1" is marked as DONE, it will copy its COUNTER
property to "Heading 2":
#+BEGIN_SRC org
,* DONE Heading 1
:PROPERTIES:
:COUNTER: 2
:TRIGGER: next-sibling chain!("COUNTER")
:END:
,* TODO Heading 2
:PROPERTIES:
:COUNTER: 2
:END:
#+END_SRC
*** Clocking
:PROPERTIES:
:CUSTOM_ID: clocking
:DESCRIPTION: Clock in or out of a target
:END:
- Syntax: clock-in!
- Syntax: clock-out!
Clocks into or out of all targets.
~clock-in!~ has no special handling of targets, so be careful when specifying
multiple targets.
In contrast, ~clock-out!~ ignores its targets and only clocks out of the current
clock, if any.
*** Property
:PROPERTIES:
:CUSTOM_ID: properties
:DESCRIPTION: Set properties of targets
:END:
- Syntax: set-property!("PROPERTY" "VALUE")
- Syntax: set-property!("PROPERTY" inc)
- Syntax: set-property!("PROPERTY" dec)
- Syntax: set-property!("PROPERTY" next)
- Syntax: set-property!("PROPERTY" prev)
- Syntax: set-property!("PROPERTY" previous)
The first form sets the property PROPERTY on all targets to VALUE.
If VALUE is a symbol, it is interpreted as follows:
- inc :: Increment a numeric property value by one
- dec :: Decrement a numeric property value by one
If either ~inc~ or ~dec~ attempt to modify a non-numeric property value, Edna will
fail with an error message.
- next :: Cycle the property through to the next allowed property value
- previous :: Cycle the property through to the previous allowed property value
The symbol ~prev~ may be used as an abbreviation for ~previous~. Similar to
~inc~ and ~dec~, any of these will fail if there are no defined properties.
When reaching the end of the list of allowed properties, ~next~ will cycle back
to the beginning.
Example:
#+begin_src org
,#+PROPERTY: TEST_ALL a b c d
,* TODO Test Heading
:PROPERTIES:
:TEST: d
:TRIGGER: self set-property!("TEST" next)
:END:
#+end_src
When "Test Heading" is set to DONE, its TEST property will change to "a". This
also works with ~previous~, but in the opposite direction.
Additionally, all special forms will fail if the property is not already set:
#+begin_src org
,* TODO Test
:PROPERTIES:
:TRIGGER: self set-property!("TEST" inc)
:END:
#+end_src
In the above example, if "Test" is set to DONE, Edna will fail to increment the
TEST property, since it doesn't exist.
- Syntax: delete-property!("PROPERTY")
Deletes the property PROPERTY from all targets.
Examples:
- set-property!("COUNTER" "1") :: Sets the property COUNTER to 1 on all targets
- set-property!("COUNTER" inc) :: Increments the property COUNTER by 1. Following the previous example, it would be 2.
*** Priority
:PROPERTIES:
:CUSTOM_ID: priorities
:DESCRIPTION: Set priorities of targets
:END:
Sets the priority of all targets.
- Syntax: set-priority!("PRIORITY")
Set the priority to the first character of PRIORITY.
- Syntax: set-priority!(up)
Cycle the target's priority up through the list of allowed priorities.
- Syntax: set-priority!(down)
Cycle the target's priority down through the list of allowed priorities.
- Syntax: set-priority!(P)
Set the target's priority to the character P.
*** Tag
:PROPERTIES:
:CUSTOM_ID: tags
:DESCRIPTION: Tags of a target
:END:
- Syntax: tag!("TAG-SPEC")
Tags all targets with TAG-SPEC, which is any valid tag specification,
e.g. tag1:tag2
*** Effort
:PROPERTIES:
:CUSTOM_ID: effort
:DESCRIPTION: So much effort!
:END:
Modifies the effort of all targets.
- Syntax: set-effort!("VALUE")
Set the effort of all targets to "VALUE".
- Syntax: set-effort!(NUMBER)
Sets the effort to the NUMBER'th allowed effort property.
- Syntax: set-effort!(increment)
Increment the effort value.
** Getting Help
:PROPERTIES:
:CUSTOM_ID: help
:DESCRIPTION: Getting some help
:END:
Edna provides help for any keyword with ~M-x org-edna-describe-keyword~. When
invoked, a list of keywords (finders, actions, etc.) known to Edna will be
provided. Select any one to get its description.
This description includes the syntax and an explanation of what the keyword
does. Some descriptions also contain examples.
* Advanced Features
:PROPERTIES:
:CUSTOM_ID: advanced
:DESCRIPTION: Be careful in here
:END:
** Finder Cache
:PROPERTIES:
:CUSTOM_ID: cache
:DESCRIPTION: Making the finders work faster
:END:
Some finders, ~match~ in particular, can take a long time to run. Oftentimes,
this can make it unappealing to use Edna at all, especially with long
checklists.
The finder cache is one solution to this. To enable it, set
~org-edna-finder-use-cache~ to non-nil. This can be done through the
customization interface, or manually with ~setq~.
When enabled, the cache will store the results of every finder form for a
configurable amount of time. This timeout is controlled by
~org-edna-finder-cache-timeout~. The cache is also invalidated if any of the
results are invalid, which can happen if their target files have been closed.
For example, if there are several entries in a checklist that all use the form
~match("daily")~ as part of their trigger, the results of that form will be
cached. When the next item is marked as DONE, the results will be searched for
in cache, not recomputed.
When reverting Org mode files, the cache will often be invalidated. This isn't
the case for every Org mode file, so we can't just tell Emacs to automatically
reset the cache when reverting a file. Instead, we provide the command
~org-edna-reset-cache~ to reset the finder cache. If you notice headings that
should be blocking but aren't while cache is enabled, reset the cache and check
again.
** Conditions
:PROPERTIES:
:CUSTOM_ID: conditions
:DESCRIPTION: More than just DONE headings
:END:
Edna gives you he option to specify *blocking conditions*. Each condition is checked
for each of the specified targets; if one of the conditions returns true for
that target, then the source heading is blocked.
If no condition is specified, ~!done?~ is used by default, which means block if
any target heading isn't done.
*** Heading is DONE
:PROPERTIES:
:CUSTOM_ID: done
:END:
- Syntax: done?
Blocks the source heading if any target heading is DONE.
*** File Has Headings
:PROPERTIES:
:CUSTOM_ID: headings
:END:
- Syntax: headings?
Blocks the source heading if any target belongs to a file that has an Org
heading. This means that target does not have to be a heading.
#+BEGIN_EXAMPLE
org-file("refile.org") headings?
#+END_EXAMPLE
The above example blocks if refile.org has any headings.
*** Heading TODO State
:PROPERTIES:
:CUSTOM_ID: todo-state
:END:
- Syntax: todo-state?(STATE)
Blocks if any target heading has TODO state set to STATE.
STATE may be a string or a symbol.
*** Lisp Variable Set
:PROPERTIES:
:CUSTOM_ID: variable-set
:END:
- Syntax: variable-set?(VARIABLE VALUE)
Evaluate VARIABLE when visiting a target, and compare it with ~equal~
against VALUE. Block the source heading if VARIABLE = VALUE.
VARIABLE should be a symbol, and VALUE is any valid lisp expression.
Examples:
- self variable-set?(test-variable 12) :: Blocks if the variable ~test-variable~ is set to 12.
- self variable-set?(buffer-file-name "org-edna.org") :: Blocks if the variable ~buffer-file-name~ is set to "org-edna.org".
*** Heading Has Property
:PROPERTIES:
:CUSTOM_ID: has-property
:END:
- Syntax: has-property?("PROPERTY" "VALUE")
Tests each target for the property PROPERTY, and blocks if it's set to VALUE.
Example:
#+begin_src org
,* TODO Take Shower
:PROPERTIES:
:COUNT: 1
:TRIGGER: self set-property!("COUNT" inc) todo!("TODO")
:END:
,* TODO Wash Towels
:PROPERTIES:
:BLOCKER: previous-sibling !has-property?("COUNT" "3")
:TRIGGER: previous-sibling set-property!("COUNT" "0")
:END:
#+end_src
In this example, "Wash Towels" can't be completed until the user has showered at
least three times.
*** Regexp Search
:PROPERTIES:
:CUSTOM_ID: re-search
:DESCRIPTION: Search for a regular expression
:END:
- Syntax: re-search?("REGEXP")
Blocks the source heading if the regular expression REGEXP is present in any
of the targets.
The targets are expected to be files, although this will work with other targets
as well. When given a target heading, the heading's file will be searched.
*** Checking Tags
:PROPERTIES:
:CUSTOM_ID: has-tags
:DESCRIPTION: Matching against a set of tags
:END:
- Syntax: has-tags?("TAG1" "TAG2" ...)
Blocks the source heading if any of the target headings have one or more of the
given tags.
#+begin_src org
,* TODO Task 1 :tag1:
,* TODO Task 2 :tag3:tag2:
,* TODO Task 3
:PROPERTIES:
:BLOCKER: rest-of-siblings-wrap has-tags?("tag1" "tag2")
:END:
#+end_src
In the above example, Tasks 1 and 2 will block Task 3. Task 1 will block it
because it contains "tag1" as one of its tags, and likewise for Task 2 and
"tag2".
Note that marking "Task 1" or "Task 2" as DONE will not unblock "Task 3". If
you want to set up such a system, use the [[#match][match]] finder.
*** Matching Headings
:PROPERTIES:
:CUSTOM_ID: matches
:DESCRIPTION: Matching against a match string
:END:
- Syntax: matches?("MATCH-STRING")
Blocks the source heading if any of the target headings match against
MATCH-STRING.
MATCH-STRING is a string passed to ~org-map-entries~.
#+begin_src org
,* TODO Task 1
,* TODO Task 2
,* TODO Task 3
:PROPERTIES:
:BLOCKER: rest-of-siblings-wrap !matches?("TODO==\"DONE\"")
:END:
#+end_src
In the above example, Tasks 1 and 2 will block Task 3 until they're marked as
DONE.
*** Negating Conditions
:PROPERTIES:
:CUSTOM_ID: negate
:DESCRIPTION: Doing the opposite
:END:
Any condition can be negated by using '!' before the condition.
#+BEGIN_EXAMPLE
match("test") !has-property?("PROP" "1")
#+END_EXAMPLE
The above example will cause the source heading to block if any heading
tagged "test" does *not* have the property PROP set to "1".
*** Multiple Conditions
:PROPERTIES:
:CUSTOM_ID: multiple
:DESCRIPTION: stacking blockers
:END:
Multiple blocking conditions can be used for a single entry. The heading will
block if any of the provided conditions evaluate to true.
#+begin_src org
,* TODO Heading 1
:PROPERTIES:
:ID: 1942caf2-caad-4757-b689-3c0029c1d8a5
:END:
,* TODO Heading 2
,* TODO Heading 3
:PROPERTIES:
:BLOCKER: previous-sibling !done? ids(1942caf2-caad-4757-b689-3c0029c1d8a5) !done?
:END:
#+end_src
"Heading 3" will block if either "Heading 1" isn't done (ids) or "Heading 2"
isn't done (previous-sibling).
** Consideration
:PROPERTIES:
:DESCRIPTION: Only some of them
:END:
"Consideration" and "consider" are special keywords that are only valid for
blockers.
A blocker says "If ANY heading in TARGETS meets CONDITION, block this task".
In order to modify the ANY part of that statement, the ~consider~ keyword may be
used:
1. consider(any)
2. consider(all)
3. consider(FRACTION)
4. consider(NUMBER)
(1) blocks the current task if any target meets the blocking condition. This is
the default case.
(2) blocks the current task only if all targets meet the blocking condition.
#+begin_src org
,* Shovel Snow
,** TODO Shovel on Monday
,** TODO Shovel on Tuesday
,** TODO Shovel on Wednesday
,** TODO Put shovel away
:PROPERTIES:
:BLOCKER: consider(all) rest-of-siblings-wrap
:END:
#+end_src
The above example blocks "Put shovel away" so long as all of the siblings are
still marked TODO.
(3) blocks the current task if at least FRACTION of the targets meet the
blocking condition.
#+begin_src org
,* Work
,** TODO Shovel Snow
,** TODO Clean room
,** TODO Vacuum
,** TODO Eat lunch
,** TODO Work on Edna
:PROPERTIES:
:BLOCKER: consider(0.5) rest-of-siblings-wrap
:END:
#+end_src
The above example blocks "Work on Edna" so long as at least half of the siblings
are marked TODO. This means that three of them must be completed before
development can begin on Edna.
(4) blocks the current task if at least NUMBER of the targets meet the blocking
condition.
#+begin_src org
,* Work
,** TODO Shovel Snow
,** TODO Clean room
,** TODO Vacuum
,** TODO Eat lunch
,** TODO Work on Edna
:PROPERTIES:
:BLOCKER: consider(2) rest-of-siblings-wrap
:END:
#+end_src
The above example blocks "Work on Edna" so long as two of the siblings are
marked TODO. This means that NUMBER=1 is the same as specifying ~any~.
A consideration must be specified before the conditions to which it applies.
Both "consider" and "consideration" are valid keywords; they both mean the same
thing.
** Conditional Forms
:PROPERTIES:
:CUSTOM_ID: conditional_forms
:DESCRIPTION: If/Then/Else
:END:
Let's say you've got the following checklist:
#+begin_src org
,* TODO Nightly
DEADLINE: <2017-12-22 Fri 22:00 +1d>
:PROPERTIES:
:ID: 12345
:BLOCKER: match("nightly")
:TRIGGER: match("nightly") todo!(TODO)
:END:
,* TODO Prepare Tomorrow's Lunch :nightly:
,* TODO Lock Back Door :nightly:
,* TODO Feed Dog :nightly:
#+end_src
You don't know in what order you want to perform each task, nor should it
matter. However, you also want the parent heading, "Nightly", to be marked as
DONE when you're finished with the last task.
There are two solutions to this: 1. Have each task attempt to mark "Nightly" as
DONE, which will spam blocking messages after each task.
The second is to use conditional forms. Conditional forms are simple; it's just
if/then/else/endif:
#+begin_quote
if CONDITION then THEN else ELSE endif
#+end_quote
Here's how that reads:
"If CONDITION would not block, execute THEN. Otherwise, execute ELSE."
For our nightly entries, this looks as follows:
#+begin_src org
,* TODO Prepare Tomorrow's Lunch :nightly:
:PROPERTIES:
:TRIGGER: if match("nightly") then ids(12345) todo!(DONE) endif
:END:
#+end_src
Thus, we replicate our original blocking condition on all of them, so it won't
trigger the original until the last one is marked DONE.
Occasionally, you may find that you'd rather execute a form if the condition
*would* block. There are two options.
The first is to use ~consider(all)~. This will tell Edna to block only if all
of the targets meets the condition, and thus not block if at least one of them
does not meet the condition. This is the opposite of Edna's standard operation,
which only allows passage if all targets meet the condition.
#+begin_src org
,* TODO Prepare Tomorrow's Lunch :nightly:
:PROPERTIES:
:TRIGGER: if consider(all) match("nightly") then ids(12345) todo!(DONE) endif
:END:
#+end_src
The second is to switch the then and else clauses:
#+begin_src org
,* TODO Prepare Tomorrow's Lunch :nightly:
:PROPERTIES:
:TRIGGER: if match("nightly") then else ids(12345) todo!(DONE) endif
:END:
#+end_src
The conditional block tells it to evaluate that section. Thus, you can
conditionally add targets, or conditionally check conditions.
** Setting the Properties
:PROPERTIES:
:DESCRIPTION: The easy way to set BLOCKER and TRIGGER
:CUSTOM_ID: setting_keywords
:END:
There are two ways to set the BLOCKER and TRIGGER properties: by hand, or the
easy way. You can probably guess which way we prefer.
With point within the heading you want to edit, type ~M-x org-edna-edit~. You end
up in a buffer that looks like this:
#+begin_example
Edit blockers and triggers in this buffer under their respective sections below.
All lines under a given section will be merged into one when saving back to
the source buffer. Finish with `C-c C-c' or abort with `C-c C-k'.
BLOCKER
BLOCKER STUFF HERE
TRIGGER
TIRGGER STUFF HERE
#+end_example
In here, you can edit the blocker and trigger properties for the original
heading in a cleaner environment. More importantly, you can complete the names
of any valid keyword within the BLOCKER or TRIGGER sections using
~completion-at-point~.
When finished, type ~C-c C-c~ to apply the changes, or ~C-c C-k~ to throw out your
changes.
* Extending Edna
:PROPERTIES:
:DESCRIPTION: What else can it do?
:END:
Extending Edna is (relatively) simple.
During operation, Edna searches for functions of the form org-edna-TYPE/KEYWORD.
** Naming Conventions
:PROPERTIES:
:DESCRIPTION: How to name new functions
:END:
In order to distinguish between actions, finders, and conditions, we add '?' to
conditions and '!' to actions. This is taken from the practice in Guile and
Scheme to suffix destructive functions with '!' and predicates with '?'.
Thus, one can have an action that files a target, and a finder that finds a
file.
We recommend that you don't name a finder with a special character at the end of
its name. As we devise new ideas, we consider using special characters for
additional categories of keywords. Thus, to avoid complications in the future,
it's best if everyone avoids using characters that may become reserved in the
future.
** Finders
:PROPERTIES:
:DESCRIPTION: Making a new finder
:END:
Finders have the form org-edna-finder/KEYWORD, like so:
#+BEGIN_SRC emacs-lisp
(defun org-edna-finder/test-finder ()
(list (point-marker)))
#+END_SRC
All finders must return a list of markers, one for each target found, or nil if
no targets were found.
** Actions
:PROPERTIES:
:DESCRIPTION: Making a new action
:END:
Actions have the form org-edna-action/KEYWORD!:
#+BEGIN_SRC emacs-lisp
(defun org-edna-action/test-action! (last-entry arg1 arg2)
)
#+END_SRC
Each action has at least one argument: ~last-entry~. This is a marker for the
current entry (not to be confused with the current target).
The rest of the arguments are the arguments specified in the form.
** Conditions
:PROPERTIES:
:DESCRIPTION: Making a new condition
:END:
#+BEGIN_SRC emacs-lisp
(defun org-edna-condition/test-cond? (neg))
#+END_SRC
All conditions have at least one argument, "NEG". If NEG is non-nil, the
condition should be negated.
Most conditions have the following form:
#+BEGIN_SRC emacs-lisp
(defun org-edna-condition/test-condition? (neg)
(let ((condition (my-test-for-condition)))
(when (org-xor condition neg)
(string-for-blocking-entry-here))))
#+END_SRC
For conditions, we return true if condition is true and neg is false, or if
condition is false and neg is true:
| cond | neg | res |
|------+-----+-----|
| t | t | f |
| t | f | t |
| f | t | t |
| f | f | f |
This is an XOR table, so we pass CONDITION and NEG into ~org-xor~ to get our
result.
A condition must return a string if the current entry should be blocked.
* Contributing
:PROPERTIES:
:DESCRIPTION: I wanna help!
:END:
We are all happy for any help you may provide.
First, check out the source code on Savannah: https://savannah.nongnu.org/projects/org-edna-el/
#+BEGIN_SRC shell
bzr branch https://bzr.savannah.gnu.org/r/org-edna-el/ org-edna
#+END_SRC
You'll also want a copy of the most recent Org Mode source:
#+BEGIN_SRC shell
git clone git://orgmode.org/org-mode.git
#+END_SRC
** Bugs
:PROPERTIES:
:CUSTOM_ID: bugs
:END:
There are two ways to submit bug reports:
1. Using the bug tracker at Savannah
2. Sending an email using ~org-edna-submit-bug-report~
When submitting a bug report, be sure to include the Edna form that caused the
bug, with as much context as possible.
** Working with EDE
:PROPERTIES:
:CUSTOM_ID: ede
:DESCRIPTION: And all its quirks
:END:
Our build system uses EDE. EDE can be a little finicky at times, but we feel
the benefits, namely package dependency handling and Makefile generation,
outweigh the costs.
One of the issues that many will likely encounter is the error "Corrupt file on
disk". This is most often due to EDE not loading all its subprojects as needed.
If you find yourself dealing with this error often, place the following in your
.emacs file:
#+begin_src emacs-lisp
;; Target types needed for working with edna
(require 'ede/proj-elisp)
(require 'ede/proj-aux)
(require 'ede/proj-misc)
#+end_src
These are the three target types that edna uses: elisp for compilation and
autoloads; aux for auxiliary files such as documentation; and misc for tests.
When creating a new file, EDE will ask if you want to add it to a target.
Consult with one of the edna devs for guidance, but usually selecting "none"
and letting one of us handle it is a good way to go.
** Compiling Edna
:PROPERTIES:
:CUSTOM_ID: compiling
:DESCRIPTION: How to compile Edna
:END:
To compile Edna, you've got to have EDE create the Makefile first. Run the
following in your Emacs instance to generate the Makefile:
#+begin_example
M-x ede-proj-regenerate
#+end_example
This will create the Makefile and point it to the correct version of Org. The
targets are as follows:
- compile :: Compiles the code. This should be done to verify that everything
will compile, as ELPA requires this.
- autoloads :: Creates the autoloads file. This should also run without
problems, so it's a good idea to check this one as well.
- check :: Runs the tests in ~org-edna-tests.el~.
To run any target, call ~make~:
#+begin_src shell
make compile autoloads
#+end_src
The above command compiles Edna and generates the autoloads file.
** Testing Edna
:PROPERTIES:
:CUSTOM_ID: testing
:DESCRIPTION: Ensuring Edna works the way we think she will
:END:
There are two ways to test Edna: the command-line and through Emacs.
The command-line version is simple, and we ask that you do any final testing
using this method. This is how we periodically check to verify that new
versions of Org mode haven't broken Edna. It uses the Makefile, which is
generated with EDE. See [[#compiling][Compiling Edna]] for how to do that. Once you have, run
~make check~ on the command line.
Edna tests are written using ~ERT~, the Emacs Regression Testing framework. In
order to use it interactively in Emacs, the following must be done:
1. Load ~org-edna-tests.el~
2. Run ~M-x ert-run-tests-interactively~
3. Select which tests to run, or just the letter "t" to run all of them.
Results are printed in their own buffer. See the ERT documentation for more
details.
** Before Sending Changes
:PROPERTIES:
:CUSTOM_ID: commit_checklist
:DESCRIPTION: Follow these instructions before sending us anything
:END:
There are a few rules to follow:
- Verify that any new Edna keywords follow the appropriate naming conventions
- Any new keywords should be documented
- We operate on headings, not headlines
- Use one word in documentation to avoid confusion
- Make sure your changes compile
- Run 'make check' to verify that your mods don't break anything
- Avoid additional or altered dependencies if at all possible
- Exception: New versions of Org mode are allowed
** Developing with Bazaar
:PROPERTIES:
:CUSTOM_ID: bzr_dev
:DESCRIPTION: How to use this strange VCS
:END:
If you're new to bazaar, we recommend using Emacs's built-in VC package. It
eases the overhead of dealing with a brand new VCS with a few standard commands.
For more information, see the info page on it (In Emacs, this is
C-h r m Introduction to VC RET).
To contribute with bazaar, you can do the following:
#+begin_src shell
# Hack away and make your changes
$ bzr commit -m "Changes I've made"
$ bzr send -o file-name.txt
#+end_src
Then, use ~org-edna-submit-bug-report~ and attach "file-name.txt". We can then
merge that into the main development branch.
** Documentation
:PROPERTIES:
:CUSTOM_ID: docs
:DESCRIPTION: Improving the documentation
:END:
Documentation is always helpful to us. Please be sure to do the following after
making any changes:
1. Update the info page in the repository with ~C-c C-e i i~
2. If you're updating the HTML documentation, switch to a theme that can easily
be read on a white background; we recommend the "adwaita" theme
* Changelog
:PROPERTIES:
:DESCRIPTION: List of changes by version
:END:
** 1.0.2
- Added ~org-edna-reset-cache~ to allow a user to reset the finder cache
- Fixed timestamp format bug with scheduled! and deadline!
- See [[#ts_format][Timestamp Format]] for more
** 1.0.1
- Fixed bug in multiple blocking conditions
** 1.0
- Various bugs fixes
- Fixed parsing of consideration
- Limited cache to just the finders that don't depend on current position
- Added "buffer" option for match finder
- Added timestamp sorting to relatives finder
- Inverted meaning of consideration to avoid confusion
- Added [[#has-tags][has-tags?]] and [[#matches][matches?]] conditions
** 1.0beta8
Quick fix for beta7.
** 1.0beta7
Biggest change here is the cache.
- Added cache to the finders to improve performance
- Updated documentation to include EDE
- Added testing and compiling documentation
** 1.0beta6
Lots of parsing fixes.
- Fixed error reporting
- Fixed parsing of negations in conditions
- Fixed parsing of multiple forms inside if/then/else blocks
** 1.0beta5
Some new forms and a new build system.
- Added new forms to set-property!
- Now allows 'inc, 'dec, 'previous, and 'next as values
- Changed build system to EDE to properly handle dependencies
- Fixed compatibility with new Org effort functions
** 1.0beta4
Just some bug fixes from the new form parsing.
- Fixed multiple forms getting incorrect targets
- Fixed multiple forms not evaluating
** 1.0beta3
HUGE addition here
- Conditional Forms
- See [[#conditional_forms][Conditional Forms]] for more information
- Overhauled Internal Parsing
- Fixed consideration keywords
- Both consider and consideration are accepted now
- Added 'any consideration
- Allows passage if just one target is fulfilled
** 1.0beta2
Big release here, with three new features.
- Added interactive keyword editor with completion
- See [[#setting_keywords][Setting the Properties]] for how to do that
- New uses of schedule! and deadline!
- New "float" form that mimics diary-float
- New "landing" addition to "+1d" and friends to force planning changes to land on a certain day or type of day (weekend/weekday)
- See [[#planning][Scheduled/Deadline]] for details
- New "relatives" finder
- Renamed from chain-find with tons of new keywords
- Modified all other relative finders (previous-sibling, first-child, etc.) to use the same keywords
- See [[#relatives][relatives]] for details
- New finders
- [[#previous-sibling-wrap][previous-sibling-wrap]]
- [[#rest-of-siblings-wrap][rest-of-siblings-wrap]]
org-edna-1.0.2/dir 0000644 0001752 0001753 00000001243 13215711055 012340 0 ustar elpa elpa This is the file .../info/dir, which contains the
topmost node of the Info hierarchy, called (dir)Top.
The first time you invoke Info you start off looking at this node.
File: dir, Node: Top This is the top of the INFO tree
This (the Directory node) gives a menu of major topics.
Typing "q" exits, "H" lists all Info commands, "d" returns here,
"h" gives a primer for first-timers,
"mEmacs" visits the Emacs manual, etc.
In Emacs, you can click mouse button 2 on a menu item or cross reference
to select it.
* Menu:
Emacs
* Org Edna: (org-edna). Extensible Dependencies ’N’ Actions for
Org Mode tasks.