Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1350 (2022-11-26 08:14:35)

feat: finish explain instruction: better output and added docs
<!--- Provide a general summary of your changes in the Title above -->

# Description

This got way too large for a single PR...

explain instruction:

- A run of multiple AND or OR filters, at the same level, generates a single AND block in the explanation, which is way more readable
- Removed the top-level `All of:` line from output - it made the output harder to read.
- For Boolean lines, include `AND`, `OR`, `NOT` or `XOR` in the explanation, for clarity
- For `NOT`, removed `None of` from the description: it only ever has one value, so does not add information.
- Included any global filter in explanation
- Included any limit instruction in explanation

Documentation:

- Document the explain instruction in new page `docs/queries/explaining-queries.md`
- Document the `plugin-tasks-query-explanation` style

Tests

- Added `tests/Query/Explain/DocsSamplesForExplain.test.ts` which uses Approval Tests to generate various output files called `*.approved.query.text` and `*.approved.explanation.text` for automatically embedding in the documentation.
- Added more tests for `explain`

Vault:

- Added examples to `resources/sample_vaults/Tasks-Demo/Filters/Explain Filters.md`

Embedding code snippets:

- Remove the 'snippet source' and 'anchor' links from embedded files. The existence of both `main` and `gh-pages` active branches on this project made them unworkable.

## Motivation and Context

Am really keen to finish off `explain` and get it released.

## How has this been tested?

- automated tests
- using the plugin in the sample vault
- generating the docs locally

## Screenshots (if appropriate)

Compare these with the screenshots in https://github.com/obsidian-tasks-group/obsidian-tasks/pull/1241

![image](https://user-images.githubusercontent.com/4840096/204079472-8968e501-4285-4433-872b-f2da8211fd50.png)

![image](https://user-images.githubusercontent.com/4840096/204079494-532b7f5f-09b8-45cf-9e64-c4f38a64c525.png)

![image](https://user-images.githubusercontent.com/4840096/204079513-7b08bf89-f420-401c-8e3a-4b2f55ffb97a.png)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [ ] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)
- [x] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))

Internal changes:

- [ ] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [x] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 64ba5729..9e4ac34e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -285,55 +285,39 @@ in some text or an object to be tested.
 Example test in `ApprovalTestsDemo.test`, that saves its input in a text file:
 
 <!-- snippet: approval-test-as-text -->
-<a id='snippet-approval-test-as-text'></a>
-
 ```ts
 test('SimpleVerify', () => {
     verify('Hello From Approvals');
 });
 ```
-
-<sup><a href='https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/tests/TestingTools/ApprovalTestsDemo.test.ts#L4-L8' title='Snippet source file'>snippet source</a> | <a href='#snippet-approval-test-as-text' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 The corresponding `approved` file, named `ApprovalTestsDemo.test.ApprovalTests_SimpleVerify.approved.txt`:
 
 <!-- snippet: ApprovalTestsDemo.test.ApprovalTests_SimpleVerify.approved.txt -->
-<a id='snippet-ApprovalTestsDemo.test.ApprovalTests_SimpleVerify.approved.txt'></a>
-
 ```txt
 Hello From Approvals
 ```
-
-<sup><a href='https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/tests/TestingTools/ApprovalTestsDemo.test.ApprovalTests_SimpleVerify.approved.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-ApprovalTestsDemo.test.ApprovalTests_SimpleVerify.approved.txt' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 <!-- snippet: approval-test-as-json -->
-<a id='snippet-approval-test-as-json'></a>
-
 ```ts
 test('JsonVerify', () => {
     const data = { name: 'fred', age: 30 };
     verifyAsJson(data);
 });
 ```
-
-<sup><a href='https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/tests/TestingTools/ApprovalTestsDemo.test.ts#L10-L15' title='Snippet source file'>snippet source</a> | <a href='#snippet-approval-test-as-json' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 The corresponding `approved` file, named `ApprovalTestsDemo.test.ApprovalTests_JsonVerify.approved.json`:
 
 <!-- snippet: ApprovalTestsDemo.test.ApprovalTests_JsonVerify.approved.json -->
-<a id='snippet-ApprovalTestsDemo.test.ApprovalTests_JsonVerify.approved.json'></a>
-
 ```json
 {
   "name": "fred",
   "age": 30
 }
 ```
-
-<sup><a href='https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/tests/TestingTools/ApprovalTestsDemo.test.ApprovalTests_JsonVerify.approved.json#L1-L4' title='Snippet source file'>snippet source</a> | <a href='#snippet-ApprovalTestsDemo.test.ApprovalTests_JsonVerify.approved.json' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 </details>
diff --git a/docs/advanced/styling.md b/docs/advanced/styling.md
index 78ac7244..acf2ec8a 100644
--- a/docs/advanced/styling.md
+++ b/docs/advanced/styling.md
@@ -11,14 +11,16 @@ has_toc: false
 Each task entry has CSS styles that allow you to change the look and feel of how the tasks are displayed. The
 following styles are available.
 
-| Class                     | Usage                                                                                                           |
-| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
-| plugin-tasks-query-result | This is applied to the UL used to hold all the tasks, each task is stored in a LI.                              |
-| plugin-tasks-list-item    | This is applied to the LI that holds each task and the INPUT element for it.                                    |
-| tasks-backlink            | This is applied to the SPAN that wraps the backlink if displayed on the task.                                   |
-| tasks-edit                | This is applied to the SPAN that wraps the edit button/icon shown next to the task that opens the task edit UI. |
-| tasks-urgency             | This is applied to the SPAN that wraps the urgency score if displayed on the task.                              |
-| task-list-item-checkbox   | This is applied to the INPUT element for the task.                                                              |
-| tasks-group-heading       | This is applied to H4, H5 and H6 group headings                                                                 |
+| Class                          | Usage                                                                                                           |
+| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
+| plugin-tasks-query-result      | This is applied to the UL used to hold all the tasks, each task is stored in a LI.                              |
+| plugin-tasks-query-explanation | This is applied to the PRE showing the query's explanation when the `explain` instruction is used.              |
+| plugin-tasks-list-item         | This is applied to the LI that holds each task and the INPUT element for it.                                    |
+| tasks-backlink                 | This is applied to the SPAN that wraps the backlink if displayed on the task.                                   |
+| tasks-edit                     | This is applied to the SPAN that wraps the edit button/icon shown next to the task that opens the task edit UI. |
+| tasks-urgency                  | This is applied to the SPAN that wraps the urgency score if displayed on the task.                              |
+| task-list-item-checkbox        | This is applied to the INPUT element for the task.                                                              |
+| tasks-group-heading            | This is applied to H4, H5 and H6 group headings                                                                 |
 
 > `tasks-group-heading` was introduced in Tasks 1.6.0.
+> `plugin-tasks-query-explanation` was introduced in Tasks 1.19.0.
diff --git a/docs/queries/examples.md b/docs/queries/examples.md
index ab633d7a..3e2fc06a 100644
--- a/docs/queries/examples.md
+++ b/docs/queries/examples.md
@@ -1,7 +1,7 @@
 ---
 layout: default
 title: Examples
-nav_order: 9
+nav_order: 10
 parent: Queries
 has_toc: false
 ---
diff --git a/docs/queries/explaining-queries.md b/docs/queries/explaining-queries.md
new file mode 100644
index 00000000..777878bd
--- /dev/null
+++ b/docs/queries/explaining-queries.md
@@ -0,0 +1,156 @@
+---
+layout: default
+title: Explaining Queries
+nav_order: 9
+parent: Queries
+has_toc: false
+---
+
+# Explaining Queries
+
+{: .no_toc }
+
+<details open markdown="block">
+  <summary>
+    Table of contents
+  </summary>
+  {: .text-delta }
+1. TOC
+{:toc}
+</details>
+
+---
+
+## Overview: the 'explain' instruction
+
+> Introduced in Tasks 1.19.0.
+
+The `explain` instruction adds some extra output at the start of the search results, when tasks blocks are viewed in Live Preview and Reading modes.
+
+This has a number of benefits:
+
+- It is easy to understand date-based-filters:
+  - Any dates in filters are expanded, to show the actual dates used in the search.
+- Boolean query logic is clearer.
+  - Combinations of queries (via `AND`, `OR`, `NOT` etc)  can be seen more clearly.
+- If there is a [global filter]({{ site.baseurl }}{% link getting-started/global-filter.md %}) enabled, it is included in the explanation.
+  - This often explains why tasks are missing from results.
+
+## Examples
+
+### Dates in filters are expanded
+
+For example, when the following text is placed in a tasks query block:
+
+<!-- snippet: DocsSamplesForExplain.test.explain_expands dates.approved.query.text -->
+```text
+starts after 2 years ago
+scheduled after 1 week ago
+due before tomorrow
+explain
+```
+<!-- endSnippet -->
+
+the results begin with the following, on `2022-10-21`:
+
+<!-- snippet: DocsSamplesForExplain.test.explain_expands dates.approved.explanation.text -->
+```text
+Explanation of this Tasks code block query:
+
+starts after 2 years ago =>
+  start date is after 2020-10-21 (Wednesday 21st October 2020) OR no start date
+
+scheduled after 1 week ago =>
+  scheduled date is after 2022-10-14 (Friday 14th October 2022)
+
+due before tomorrow =>
+  due date is before 2022-10-22 (Saturday 22nd October 2022)
+```
+<!-- endSnippet -->
+
+Note how it shows the dates being searched for very clearly, including the day of the week.
+
+It also shows that `starts` searches also match tasks with not start date.
+
+### Boolean combinations are displayed
+
+For example, when the following text is placed in a tasks query block:
+
+<!-- snippet: DocsSamplesForExplain.test.explain_boolean combinations.approved.query.text -->
+```text
+explain
+not done
+(due before tomorrow) AND (is recurring)
+```
+<!-- endSnippet -->
+
+the results begin with the following, on `2022-10-21`:
+
+<!-- snippet: DocsSamplesForExplain.test.explain_boolean combinations.approved.explanation.text -->
+```text
+Explanation of this Tasks code block query:
+
+not done
+
+(due before tomorrow) AND (is recurring) =>
+  AND (All of):
+    due date is before 2022-10-22 (Saturday 22nd October 2022)
+    is recurring
+```
+<!-- endSnippet -->
+
+### More complex combinations are displayed
+
+With complex Boolean combinations of filters, it is easy to get parentheses in the wrong place. With `explain`, the interpreted logic is easily visible.
+
+For example, when the following text is placed in a tasks query block:
+
+<!-- snippet: DocsSamplesForExplain.test.explain_nested boolean combinations.approved.query.text -->
+```text
+explain
+( (description includes 1) AND (description includes 2) AND (description includes 3) ) OR ( (description includes 5) AND (description includes 6) AND (description includes 7) ) AND NOT (description includes 7)
+```
+<!-- endSnippet -->
+
+the results begin with the following, on `2022-10-21`:
+
+<!-- snippet: DocsSamplesForExplain.test.explain_nested boolean combinations.approved.explanation.text -->
+```text
+Explanation of this Tasks code block query:
+
+( (description includes 1) AND (description includes 2) AND (description includes 3) ) OR ( (description includes 5) AND (description includes 6) AND (description includes 7) ) AND NOT (description includes 7) =>
+  OR (At least one of):
+    AND (All of):
+      description includes 1
+      description includes 2
+      description includes 3
+    AND (All of):
+      AND (All of):
+        description includes 5
+        description includes 6
+        description includes 7
+      NOT:
+        description includes 7
+```
+<!-- endSnippet -->
+
+## Styling explain results
+
+### Default style
+
+For readability, explanations are shown in a fixed-width font (a `PRE` block), and if the text is too wide for the screen a horizontal scrollbar is shown. Otherwise, testing showed that the explanations would be unusable on small-screen devices.
+
+### Customizing the results
+
+Using a [CSS snippet in Obsidian](https://help.obsidian.md/How+to/Add+custom+styles#Use+Themes+and+or+CSS+snippets), we can change the appearance of the explanation block.
+
+For example, [this CSS snippet](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/gh-pages/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-explain-text-blue.css) `tasks-plugin-explain-text-blue.css` makes the explanation block text blue:
+
+<!-- snippet: resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-explain-text-blue.css -->
+```css
+/* Make the Tasks plugin's 'explain' output stand out in blue */
+.plugin-tasks-query-explanation {
+    color: var(--color-blue);
+}
+```
+<!-- endSnippet -->
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index b1044a19..188f920b 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -46,7 +46,10 @@ This table summarizes the filters and other options available inside a `tasks` b
 | **Other Filter Options**                                                                                                                                                                            |                                             |                       |                        |
 | `exclude sub-items`                                                                                                                                                                                 |                                             |                       |                        |
 | `limit to <number> tasks`<br>`limit <number>`                                                                                                                                                       |                                             |                       |                        |
-| **Other Layout options**                                                                                                                                                                            |                                             |                       |                        |
+| **Other Layout Options**                                                                                                                                                                            |                                             |                       |                        |
 | `hide edit button`                                                                                                                                                                                  |                                             |                       |                        |
 | `hide task count`                                                                                                                                                                                   |                                             |                       |                        |
 | `short mode`                                                                                                                                                                                        |                                             |                       |                        |
+| **Other Instructions**                                                                                                                                                                              |                                             |                       |                        |
+| `explain`                                                                                                                                                                                           |                                             |                       |                        |
+| `# comment`                                                                                                                                                                                         |                                             |                       |                        |
diff --git a/mdsnippets.json b/mdsnippets.json
index 453362b1..f268e88e 100644
--- a/mdsnippets.json
+++ b/mdsnippets.json
@@ -3,5 +3,6 @@
   "ExcludeDirectories": [ "node_modules", "resources" ],
   "Convention": "InPlaceOverwrite",
   "TocLevel": 5,
+  "OmitSnippetLinks": true,
   "UrlPrefix": "https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main"
 }
diff --git a/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-explain-text-blue.css b/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-explain-text-blue.css
index b87495c3..2ad6103e 100644
--- a/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-explain-text-blue.css
+++ b/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-explain-text-blue.css
@@ -1,3 +1,4 @@
-.plugin-tasks-query-explanation{
+/* Make the Tasks plugin's 'explain' output stand out in blue */
+.plugin-tasks-query-explanation {
     color: var(--color-blue);
 }
diff --git a/resources/sample_vaults/Tasks-Demo/Filters/Explain Filters.md b/resources/sample_vaults/Tasks-Demo/Filters/Explain Filters.md
index b9426833..da153b48 100644
--- a/resources/sample_vaults/Tasks-Demo/Filters/Explain Filters.md
+++ b/resources/sample_vaults/Tasks-Demo/Filters/Explain Filters.md
@@ -32,3 +32,26 @@ explain
 (description includes 1) AND (description includes 2) AND (description includes 3) AND (description includes 4)
 limit 1
 ```
+
+## Nested Boolean logic with NOT
+
+```tasks
+explain
+NOT ( (description includes 1) AND (description includes 2) AND (description includes 3) AND (description includes 4) )
+limit 1
+```
+
+## One filter
+
+```tasks
+explain
+starts before today
+limit 1
+```
+
+## No filters
+
+```tasks
+explain
+limit 1
+```
diff --git a/src/Query/Explain/Explanation.ts b/src/Query/Explain/Explanation.ts
index 69d2424f..9f8d8b68 100644
--- a/src/Query/Explain/Explanation.ts
+++ b/src/Query/Explain/Explanation.ts
@@ -7,11 +7,13 @@
  * Later, more human-readable descriptions will be generated.
  */
 export class Explanation {
-    public description: string;
-    public children: Explanation[];
+    public readonly description: string;
+    public readonly symbol: string; // AND, OR, NOT, XOR
+    public readonly children: Explanation[];
 
-    constructor(description: string, children: Explanation[] = []) {
+    constructor(description: string, children: Explanation[] = [], symbol: string = '') {
         this.description = description;
+        this.symbol = symbol;
         this.children = children;
     }
 
@@ -20,7 +22,7 @@ export class Explanation {
      * @param children
      */
     public static booleanAnd(children: Explanation[]) {
-        return new Explanation('All of', children);
+        return this.combineOrCreateExplanation('All of', children, 'AND');
     }
 
     /**
@@ -28,7 +30,7 @@ export class Explanation {
      * @param children
      */
     public static booleanOr(children: Explanation[]) {
-        return new Explanation('At least one of', children);
+        return this.combineOrCreateExplanation('At least one of', children, 'OR');
     }
 
     /**
@@ -36,7 +38,7 @@ export class Explanation {
      * @param children
      */
     public static booleanNot(children: Explanation[]) {
-        return new Explanation('None of', children);
+        return new Explanation('None of', children, 'NOT');
     }
 
     /**
@@ -44,7 +46,7 @@ export class Explanation {
      * @param children
      */
     public static booleanXor(children: Explanation[]) {
-        return new Explanation('Exactly one of', children);
+        return new Explanation('Exactly one of', children, 'XOR');
     }
 
     /**
@@ -55,13 +57,17 @@ export class Explanation {
      * @param currentIndentation - This is an implementation detail. Users can ignore it.
      */
     public asString(currentIndentation: string = '') {
-        let result = currentIndentation + this.description;
         if (this.children.length == 0) {
-            // No children, so just return
-            return result;
+            return currentIndentation + this.description;
         }
 
         // We have children, so concatenate them together
+        let result = currentIndentation + `${this.symbol}`;
+        if (this.children.length > 1) {
+            // The descriptions like 'All of', 'None of' are one really meaningful
+            // if there is more than one filter. Otherwise, they are just confusing.
+            result += ` (${this.description})`;
+        }
         result += ':';
         const newIndentation = currentIndentation + '  ';
         for (let i = 0; i < this.children.length; i++) {
@@ -69,4 +75,16 @@ export class Explanation {
         }
         return result;
     }
+
+    private static combineOrCreateExplanation(description: string, children: Explanation[], symbol: string) {
+        if (children.length === 2) {
+            const child0 = children[0];
+            const child1 = children[1];
+            if (child0.symbol === symbol && child1.symbol === '') {
+                child0.children.push(child1);
+                return child0;
+            }
+        }
+        return new Explanation(description, children, symbol);
+    }
 }
diff --git a/src/Query/Filter/BooleanField.ts b/src/Query/Filter/BooleanField.ts
index 21f56b74..4576d735 100644
--- a/src/Query/Filter/BooleanField.ts
+++ b/src/Query/Filter/BooleanField.ts
@@ -175,31 +175,30 @@ export class BooleanField extends Field {
      */
     private constructExplanation(postfixExpression: PostfixExpression): Explanation {
         // For an explanation of the code, see the JSdoc and comments of filterTaskWithParsedQuery()
-        const booleanStack: Explanation[] = [];
-        // TODO Review comments
+        const explanationStack: Explanation[] = [];
         for (const token of postfixExpression) {
             if (token.name === 'IDENTIFIER') {
                 if (token.value == null) throw Error('null token value'); // This should not happen
                 const filter = this.subFields[token.value.trim()];
-                booleanStack.push(filter.explanation);
+                explanationStack.push(filter.explanation);
             } else if (token.name === 'OPERATOR') {
                 // To evaluate an operator we need to pop the required number of items from the boolean stack,
                 // do the logical evaluation and push back the result
                 if (token.value === 'NOT') {
-                    const arg1 = booleanStack.pop();
-                    booleanStack.push(Explanation.booleanNot([arg1!]));
+                    const arg1 = explanationStack.pop();
+                    explanationStack.push(Explanation.booleanNot([arg1!]));
                 } else if (token.value === 'OR') {
-                    const arg2 = booleanStack.pop();
-                    const arg1 = booleanStack.pop();
-                    booleanStack.push(Explanation.booleanOr([arg1!, arg2!]));
+                    const arg2 = explanationStack.pop();
+                    const arg1 = explanationStack.pop();
+                    explanationStack.push(Explanation.booleanOr([arg1!, arg2!]));
                 } else if (token.value === 'AND') {
-                    const arg2 = booleanStack.pop();
-                    const arg1 = booleanStack.pop();
-                    booleanStack.push(Explanation.booleanAnd([arg1!, arg2!]));
+                    const arg2 = explanationStack.pop();
+                    const arg1 = explanationStack.pop();
+                    explanationStack.push(Explanation.booleanAnd([arg1!, arg2!]));
                 } else if (token.value === 'XOR') {
-                    const arg2 = booleanStack.pop();
-                    const arg1 = booleanStack.pop();
-                    booleanStack.push(Explanation.booleanXor([arg1!, arg2!]));
+                    const arg2 = explanationStack.pop();
+                    const arg1 = explanationStack.pop();
+                    explanationStack.push(Explanation.booleanXor([arg1!, arg2!]));
                 } else {
                     throw Error('Unsupported operator: ' + token.value);
                 }
@@ -208,6 +207,6 @@ export class BooleanField extends Field {
             }
         }
         // Eventually the Explanation is the only item left in the boolean stack
-        return booleanStack[0];
+        return explanationStack[0];
     }
 }
diff --git a/src/Query/Filter/Filter.ts b/src/Query/Filter/Filter.ts
index 843f31a2..fe843170 100644
--- a/src/Query/Filter/Filter.ts
+++ b/src/Query/Filter/Filter.ts
@@ -27,6 +27,16 @@ export class Filter {
         this.explanation = explanation;
         this.filterFunction = filterFunction;
     }
+
+    public explainFilterIndented(indent: string) {
+        const explanation = this.explanation;
+        const unindentedExplanation = explanation.asString();
+        if (unindentedExplanation === this.instruction) {
+            return `${indent}${this.instruction}\n`;
+        } else {
+            return `${indent}${this.instruction} =>\n${explanation.asString('  ')}\n`;
+        }
+    }
 }
 
 /**
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index 155fb619..07896f46 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -1,6 +1,7 @@
 import { LayoutOptions } from '../LayoutOptions';
 import type { Task } from '../Task';
 import type { IQuery } from '../IQuery';
+import { getSettings } from '../Config/Settings';
 import { Sort } from './Sort';
 import type { TaskGroups } from './TaskGroups';
 import { parseFilter } from './FilterParser';
@@ -109,27 +110,35 @@ export class Query implements IQuery {
     }
 
     public explainQuery(): string {
-        // TODO Include limit, if any
-        // TODO Include global filter, if any
-        // TODO State today's date (and maybe weekday)
+        return 'Explanation of this Tasks code block query:\n\n' + this.explainQueryWithoutIntroduction();
+    }
+
+    public explainQueryWithoutIntroduction(): string {
+        let result = '';
+
+        const { globalFilter } = getSettings();
+        if (globalFilter.length !== 0) {
+            result += `Only tasks containing the global filter '${globalFilter}'.\n\n`;
+        }
 
         const numberOfFilters = this.filters.length;
         if (numberOfFilters === 0) {
-            return 'No filters supplied. All tasks will match the query.';
+            result += 'No filters supplied. All tasks will match the query.';
+        } else {
+            for (let i = 0; i < numberOfFilters; i++) {
+                if (i > 0) result += '\n';
+                result += this.filters[i].explainFilterIndented('');
+            }
         }
 
-        let result = 'All of:\n';
-        for (let i = 0; i < numberOfFilters; i++) {
-            if (i > 0) result += '\n';
-            const filter = this.filters[i];
-            const explanation = filter.explanation;
-            const unindentedExplanation = explanation.asString();
-            if (unindentedExplanation === filter.instruction) {
-                result += `  ${filter.instruction}\n`;
-            } else {
-                result += `  ${filter.instruction} =>\n${explanation.asString('    ')}\n`;
+        if (this._limit !== undefined) {
+            result += `\n\nAt most ${this._limit} task`;
+            if (this._limit !== 1) {
+                result += 's';
             }
+            result += '.\n';
         }
+
         return result;
     }
 
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index 8221d5b3..edaa5e87 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -171,7 +171,7 @@ class QueryRenderChild extends MarkdownRenderChild {
 
         const explanationsBlock = content.createEl('pre');
         explanationsBlock.addClasses(['plugin-tasks-query-explanation']);
-        explanationsBlock.setText('Explanation of query:\n\n' + explanationAsString);
+        explanationsBlock.setText(explanationAsString);
         content.appendChild(explanationsBlock);
     }
 


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index f4658b71..5395a376 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -4,6 +4,7 @@
 import moment from 'moment';
 import { Query } from '../src/Query/Query';
 import { Priority, Status, Task } from '../src/Task';
+import { resetSettings, updateSettings } from '../src/Config/Settings';
 import { createTasksFromMarkdown, fromLine } from './TestHelpers';
 import { shouldSupportFiltering } from './TestingTools/FilterTestHelpers';
 import type { FilteringCase } from './TestingTools/FilterTestHelpers';
@@ -763,35 +764,95 @@ describe('Query', () => {
     });
 
     describe('explanations', () => {
+        afterEach(() => {
+            resetSettings();
+        });
+
         it('should explain 0 filters', () => {
             const input = '';
             const query = new Query({ source: input });
 
             const expectedDisplayText = 'No filters supplied. All tasks will match the query.';
-            expect(query.explainQuery()).toEqual(expectedDisplayText);
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
+        });
+
+        it('should explain 0 filters with global filter', () => {
+            updateSettings({ globalFilter: '#task' });
+
+            const input = '';
+            const query = new Query({ source: input });
+
+            const expectedDisplayText = `Only tasks containing the global filter '#task'.
+
+No filters supplied. All tasks will match the query.`;
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
         });
 
         it('should explain 1 filter', () => {
             const input = 'description includes hello';
             const query = new Query({ source: input });
 
-            const expectedDisplayText = `All of:
-  description includes hello
+            const expectedDisplayText = `description includes hello
+`;
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
+        });
+
+        it('should explain 1 filter', () => {
+            updateSettings({ globalFilter: '#task' });
+
+            const input = 'description includes hello';
+            const query = new Query({ source: input });
+
+            const expectedDisplayText = `Only tasks containing the global filter '#task'.
+
+description includes hello
 `;
-            expect(query.explainQuery()).toEqual(expectedDisplayText);
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
         });
 
         it('should explain 2 filters', () => {
             const input = 'description includes hello\ndue 2012-01-23';
             const query = new Query({ source: input });
 
-            const expectedDisplayText = `All of:
-  description includes hello
+            const expectedDisplayText = `description includes hello
+
+due 2012-01-23 =>
+  due date is on 2012-01-23 (Monday 23rd January 2012)
+`;
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
+        });
+
+        it('should explain limit 5', () => {
+            const input = 'limit 5';
+            const query = new Query({ source: input });
+
+            const expectedDisplayText = `No filters supplied. All tasks will match the query.
+
+At most 5 tasks.
+`;
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
+        });
+
+        it('should explain limit 1', () => {
+            const input = 'limit 1';
+            const query = new Query({ source: input });
+
+            const expectedDisplayText = `No filters supplied. All tasks will match the query.
+
+At most 1 task.
+`;
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
+        });
+
+        it('should explain limit 0', () => {
+            const input = 'limit 0';
+            const query = new Query({ source: input });
+
+            const expectedDisplayText = `No filters supplied. All tasks will match the query.
 
-  due 2012-01-23 =>
-    due date is on 2012-01-23 (Monday 23rd January 2012)
+At most 0 tasks.
 `;
-            expect(query.explainQuery()).toEqual(expectedDisplayText);
+            expect(query.explainQueryWithoutIntroduction()).toEqual(expectedDisplayText);
         });
     });
 
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean combinations.approved.explanation.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean combinations.approved.explanation.text
new file mode 100644
index 00000000..21cd27ac
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean combinations.approved.explanation.text
@@ -0,0 +1,8 @@
+Explanation of this Tasks code block query:
+
+not done
+
+(due before tomorrow) AND (is recurring) =>
+  AND (All of):
+    due date is before 2022-10-22 (Saturday 22nd October 2022)
+    is recurring
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean combinations.approved.query.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean combinations.approved.query.text
new file mode 100644
index 00000000..81228920
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean combinations.approved.query.text
@@ -0,0 +1,4 @@
+
+explain
+not done
+(due before tomorrow) AND (is recurring)
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_expands dates.approved.explanation.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_expands dates.approved.explanation.text
new file mode 100644
index 00000000..f52200c1
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_expands dates.approved.explanation.text
@@ -0,0 +1,10 @@
+Explanation of this Tasks code block query:
+
+starts after 2 years ago =>
+  start date is after 2020-10-21 (Wednesday 21st October 2020) OR no start date
+
+scheduled after 1 week ago =>
+  scheduled date is after 2022-10-14 (Friday 14th October 2022)
+
+due before tomorrow =>
+  due date is before 2022-10-22 (Saturday 22nd October 2022)
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_expands dates.approved.query.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_expands dates.approved.query.text
new file mode 100644
index 00000000..149a4c8e
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_expands dates.approved.query.text
@@ -0,0 +1,5 @@
+
+starts after 2 years ago
+scheduled after 1 week ago
+due before tomorrow
+explain
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_nested boolean combinations.approved.explanation.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_nested boolean combinations.approved.explanation.text
new file mode 100644
index 00000000..28365e62
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_nested boolean combinations.approved.explanation.text
@@ -0,0 +1,15 @@
+Explanation of this Tasks code block query:
+
+( (description includes 1) AND (description includes 2) AND (description includes 3) ) OR ( (description includes 5) AND (description includes 6) AND (description includes 7) ) AND NOT (description includes 7) =>
+  OR (At least one of):
+    AND (All of):
+      description includes 1
+      description includes 2
+      description includes 3
+    AND (All of):
+      AND (All of):
+        description includes 5
+        description includes 6
+        description includes 7
+      NOT:
+        description includes 7
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.explain_nested boolean combinations.approved.query.text b/tests/Query/Explain/DocsSamplesForExplain.test.explain_nested boolean combinations.approved.query.text
new file mode 100644
index 00000000..9c3a39ce
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_nested boolean combinations.approved.query.text
@@ -0,0 +1,3 @@
+
+explain
+( (description includes 1) AND (description includes 2) AND (description includes 3) ) OR ( (description includes 5) AND (description includes 6) AND (description includes 7) ) AND NOT (description includes 7)
diff --git a/tests/Query/Explain/DocsSamplesForExplain.test.ts b/tests/Query/Explain/DocsSamplesForExplain.test.ts
new file mode 100644
index 00000000..b6d4edb7
--- /dev/null
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.ts
@@ -0,0 +1,87 @@
+/**
+ * @jest-environment jsdom
+ */
+import { verify } from 'approvals/lib/Providers/Jest/JestApprovals';
+import moment from 'moment';
+import { Options } from 'approvals/lib/Core/Options';
+import { Query } from '../../../src/Query/Query';
+
+window.moment = moment;
+
+/**
+ * Save an instructions block to disc, so that it can be embedded in
+ * to documentation, using a 'snippet' line.
+ * @todo Figure out how to include the '```tasks' and '```' lines:
+ *       see discussion in https://github.com/SimonCropp/MarkdownSnippets/issues/537
+ * @param instructions
+ * @param options
+ */
+function verifyQuery(instructions: string, options?: Options): void {
+    options = options || new Options();
+    options = options.forFile().withFileExtention('query.text');
+    verify(instructions, options);
+}
+
+/**
+ * Save an explanation of the instructions block to disk, so that it can be
+ * embedded in to documentation, using a 'snippet' line.
+ * @param instructions
+ * @param options
+ */
+function verifyExplanation(instructions: string, options?: Options): void {
+    const query = new Query({ source: instructions });
+    const explanation = query.explainQuery();
+
+    expect(query.error).toBeUndefined();
+
+    options = options || new Options();
+    options = options.forFile().withFileExtention('explanation.text');
+    verify(explanation, options);
+}
+
+describe('explain', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2022, 9, 21)); // 2022-10-21
+    });
+
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
+    it('expands dates', () => {
+        // Arrange
+        const instructions: string = `
+starts after 2 years ago
+scheduled after 1 week ago
+due before tomorrow
+explain`;
+
+        // Act, Assert
+        verifyQuery(instructions);
+        verifyExplanation(instructions);
+    });
+
+    it('boolean combinations', () => {
+        // Arrange
+        const instructions: string = `
+explain
+not done
+(due before tomorrow) AND (is recurring)`;
+
+        // Act, Assert
+        verifyQuery(instructions);
+        verifyExplanation(instructions);
+    });
+
+    it('nested boolean combinations', () => {
+        // Arrange
+        const instructions: string = `
+explain
+( (description includes 1) AND (description includes 2) AND (description includes 3) ) OR ( (description includes 5) AND (description includes 6) AND (description includes 7) ) AND NOT (description includes 7)`;
+
+        // Act, Assert
+        verifyQuery(instructions);
+        verifyExplanation(instructions);
+    });
+});
diff --git a/tests/Query/Explain/Explain.test.ts b/tests/Query/Explain/Explain.test.ts
index 4ddbad69..70f85e1f 100644
--- a/tests/Query/Explain/Explain.test.ts
+++ b/tests/Query/Explain/Explain.test.ts
@@ -5,6 +5,7 @@ describe('Explain', () => {
         const description = 'due date is before 2022-10-28';
         const explanation = new Explanation(description);
         expect(explanation.description).toEqual(description);
+        expect(explanation.symbol).toEqual('');
         expect(explanation.children).toEqual([]);
         expect(explanation.asString()).toEqual(description);
     });
@@ -13,8 +14,9 @@ describe('Explain', () => {
         const children: Explanation[] = [new Explanation('x includes A'), new Explanation('x includes B')];
         const explanation = Explanation.booleanAnd(children);
         expect(explanation.description).toEqual('All of');
+        expect(explanation.symbol).toEqual('AND');
         expect(explanation.children).toEqual(children);
-        const expected = `All of:
+        const expected = `AND (All of):
   x includes A
   x includes B`;
         expect(explanation.asString()).toEqual(expected);
@@ -24,6 +26,7 @@ describe('Explain', () => {
         const children: Explanation[] = [new Explanation('x includes A'), new Explanation('x includes B')];
         const explanation = Explanation.booleanOr(children);
         expect(explanation.description).toEqual('At least one of');
+        expect(explanation.symbol).toEqual('OR');
         expect(explanation.children).toEqual(children);
     });
 
@@ -31,6 +34,7 @@ describe('Explain', () => {
         const children: Explanation[] = [new Explanation('x includes A'), new Explanation('x includes B')];
         const explanation = Explanation.booleanNot(children);
         expect(explanation.description).toEqual('None of');
+        expect(explanation.symbol).toEqual('NOT');
         expect(explanation.children).toEqual(children);
     });
 
@@ -38,6 +42,7 @@ describe('Explain', () => {
         const children: Explanation[] = [new Explanation('x includes A'), new Explanation('x includes B')];
         const explanation = Explanation.booleanXor(children);
         expect(explanation.description).toEqual('Exactly one of');
+        expect(explanation.symbol).toEqual('XOR');
         expect(explanation.children).toEqual(children);
     });
 
@@ -45,11 +50,11 @@ describe('Explain', () => {
         const not = Explanation.booleanOr([new Explanation('x1 includes A'), new Explanation('x1 includes B')]);
         const or = Explanation.booleanOr([new Explanation('x2 includes C'), new Explanation('x2 includes D')]);
         const and = Explanation.booleanAnd([not, or]);
-        const expected = `All of:
-  At least one of:
+        const expected = `AND (All of):
+  OR (At least one of):
     x1 includes A
     x1 includes B
-  At least one of:
+  OR (At least one of):
     x2 includes C
     x2 includes D`;
         expect(and.asString()).toEqual(expected);
diff --git a/tests/Query/Filter/BooleanField.test.ts b/tests/Query/Filter/BooleanField.test.ts
index afc25dd1..72ec3f09 100644
--- a/tests/Query/Filter/BooleanField.test.ts
+++ b/tests/Query/Filter/BooleanField.test.ts
@@ -161,7 +161,7 @@ describe('explain boolean queries', () => {
     it('should explain Boolean AND', () => {
         const instruction = '(description includes d1) AND (priority medium)';
         const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
-        const expected = `All of:
+        const expected = `AND (All of):
   description includes d1
   priority is medium`;
         expect(filterOrMessage).toHaveExplanation(expected);
@@ -170,7 +170,7 @@ describe('explain boolean queries', () => {
     it('should explain Boolean OR', () => {
         const instruction = '(description includes d1) OR (priority medium)';
         const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
-        const expected = `At least one of:
+        const expected = `OR (At least one of):
   description includes d1
   priority is medium`;
         expect(filterOrMessage).toHaveExplanation(expected);
@@ -179,13 +179,13 @@ describe('explain boolean queries', () => {
     it('should explain Boolean NOT', () => {
         const instruction = 'NOT (description includes d1)';
         const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
-        expect(filterOrMessage).toHaveExplanation('None of:\n  description includes d1');
+        expect(filterOrMessage).toHaveExplanation('NOT:\n  description includes d1');
     });
 
     it('should explain Boolean XOR', () => {
         const instruction = '(description includes d1) XOR (priority medium)';
         const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
-        const expected = `Exactly one of:
+        const expected = `XOR (Exactly one of):
   description includes d1
   priority is medium`;
         expect(filterOrMessage).toHaveExplanation(expected);
@@ -194,39 +194,119 @@ describe('explain boolean queries', () => {
     it('should explain 2 Boolean ORs', () => {
         const instruction = '(description includes d1) OR (description includes d2) OR (priority medium)';
         const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
-        // TODO This creates a nested 'At least one of' inside the first one.
-        //      It's technically correct, but hard for users to read.
-        //      I would like to try and somehow collate, or run together, runs of AND and OR operations
-        const expected = `At least one of:
-  At least one of:
-    description includes d1
-    description includes d2
+        const expected = `OR (At least one of):
+  description includes d1
+  description includes d2
   priority is medium`;
         expect(filterOrMessage).toHaveExplanation(expected);
     });
 
+    it('should explain 3 Boolean ANDs', () => {
+        const instruction = '(description includes 1) AND (description includes 2) AND (description includes 3)';
+        const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
+        expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
+            "AND (All of):
+              description includes 1
+              description includes 2
+              description includes 3"
+        `);
+    });
+
     it('should explain 9 Boolean ANDs', () => {
         const instruction =
             '(description includes 1) AND (description includes 2) AND (description includes 3) AND (description includes 4) AND (description includes 5) AND (description includes 6) AND (description includes 7) AND (description includes 8) AND (description includes 9)';
         const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
         expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
-            "All of:
-              All of:
-                All of:
-                  All of:
-                    All of:
-                      All of:
-                        All of:
-                          All of:
-                            description includes 1
-                            description includes 2
-                          description includes 3
-                        description includes 4
-                      description includes 5
-                    description includes 6
-                  description includes 7
-                description includes 8
+            "AND (All of):
+              description includes 1
+              description includes 2
+              description includes 3
+              description includes 4
+              description includes 5
+              description includes 6
+              description includes 7
+              description includes 8
               description includes 9"
         `);
     });
+
+    it('( a && b ) && c', () => {
+        const instruction = '( (description includes a) AND (description includes b) ) AND (description includes c)';
+        const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
+        expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
+            "AND (All of):
+              description includes a
+              description includes b
+              description includes c"
+        `);
+    });
+
+    it('a && ( b && c )', () => {
+        const instruction = '( description includes a ) AND ( (description includes b) AND (description includes c) )';
+        const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
+        expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
+            "AND (All of):
+              description includes a
+              AND (All of):
+                description includes b
+                description includes c"
+        `);
+    });
+
+    it('( a || b ) || c', () => {
+        const instruction = '( (description includes a) OR (description includes b) ) OR (description includes c)';
+        const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
+        expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
+            "OR (At least one of):
+              description includes a
+              description includes b
+              description includes c"
+        `);
+    });
+
+    it('a || ( b || c )', () => {
+        const instruction = '( description includes a ) OR ( (description includes b) OR (description includes c) )';
+        const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
+        expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
+            "OR (At least one of):
+              description includes a
+              OR (At least one of):
+                description includes b
+                description includes c"
+        `);
+    });
+
+    it('( a && b && c ) || ( d && e && f )', () => {
+        const instruction =
+            '( (description includes a) AND (description includes b) AND (description includes c) ) OR ( (description includes d) AND (description includes e) AND (description includes f) )';
+        const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
+        expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
+            "OR (At least one of):
+              AND (All of):
+                description includes a
+                description includes b
+                description includes c
+              AND (All of):
+                description includes d
+                description includes e
+                description includes f"
+        `);
+    });
+
+    it('( a || b || c ) && ( d || e || f )', () => {
+        const instruction =
+            '( (description includes a) OR (description includes b) OR (description includes c) ) AND ( (description includes d) OR (description includes e) OR (description includes f) )';
+        const filterOrMessage = new BooleanField().createFilterOrErrorMessage(instruction);
+        expect(filterOrMessage.filter?.explanation.asString()).toMatchInlineSnapshot(`
+            "AND (All of):
+              OR (At least one of):
+                description includes a
+                description includes b
+                description includes c
+              OR (At least one of):
+                description includes d
+                description includes e
+                description includes f"
+        `);
+    });
 });
diff --git a/tests/Query/Filter/PriorityField.test.ts b/tests/Query/Filter/PriorityField.test.ts
index a678b82f..11df020a 100644
--- a/tests/Query/Filter/PriorityField.test.ts
+++ b/tests/Query/Filter/PriorityField.test.ts
@@ -77,7 +77,7 @@ describe('priority error cases', () => {
 });
 
 describe('explain priority', () => {
-    it('simple case just repeats the-supplied line', () => {
+    it('simple case just repeats the supplied line', () => {
         const field = new PriorityField();
         const instruction = 'priority above none';
         const filterOrMessage = field.createFilterOrErrorMessage(instruction);
diff --git a/tests/Query/Filter/StartDateField.test.ts b/tests/Query/Filter/StartDateField.test.ts
index a17fea15..ec5a58da 100644
--- a/tests/Query/Filter/StartDateField.test.ts
+++ b/tests/Query/Filter/StartDateField.test.ts
@@ -19,7 +19,7 @@ describe('explain start date queries', () => {
         );
     });
 
-    it('implicit "on" gets added to explanation', () => {
+    it('implicit "on" gets added to explanation, and it is clear that start date is optional', () => {
         const filterOrMessage = new StartDateField().createFilterOrErrorMessage('starts 2023-01-02');
         expect(filterOrMessage).toHaveExplanation(
             'start date is on 2023-01-02 (Monday 2nd January 2023) OR no start date',