Repository: obsidian-tasks-group/obsidian-tasks (ts)

Issue:
Incomplete explanation of regular expression filters - and other multi-line explanations - when in Boolean instructions
### Please check that this issue hasn't been reported before.

- [X] I searched previous [Bug Reports](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22+sort%3Acomments-desc) didn't find any similar reports.

### Expected Behavior

Regular expression searches should be explained consistently, regardless of whether or not they are included in a Boolean query.

### Current behaviour

The first line of the explanation of regular expression explanations is lost when they are in Boolean queries.

<img width="755" alt="image" src="https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/3bd4f2ee-e549-4fde-abfa-70610c3dc18e">


### Steps to reproduce

1. Create a note with this Tasks block:

~~~
```tasks
description regex matches /buy/i
path includes Home/Shopping

( description regex matches /buy/i ) AND ( path includes Home/Shopping )

explain
```
~~~

2. View the output in Reading or Live Preview modes:

**Expected output**

```
  ( description regex matches /buy/i ) AND ( path includes Home/Shopping ) =>
    AND (All of):
      description regex matches /buy/i =>
        using regex:            'buy' with flag 'i'
      path includes Home/Shopping
```

**Actual output**

```
  ( description regex matches /buy/i ) AND ( path includes Home/Shopping ) =>
    AND (All of):
      using regex:            'buy' with flag 'i'
      path includes Home/Shopping
```



### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

1.5.11

### Tasks Plugin Version

6.1.2

### Checks

- [ ] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

_No response_

Pull request:
fix: 'explain' omitted the instruction line in some Boolean expressions
<!--- Provide a general summary of your changes in the Title above -->

# Description

Fix the `explain` output for filters inside Boolean expressions, where the filter's explanation was more than a single line.

Previously, the source line was not displayed in that situation, and now it is.

## Motivation and Context

Fix #2707

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

## How has this been tested?

- Fix the failing test that I added in 0328f4926ee31e9c011c1dae01955d5600e005e0 to demonstrate this bug.
- Update the results of a few existing tests.
- Exploratory testing.

## Screenshots (if appropriate)

![image](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/6e1f1eca-19ae-4117-b00c-06511b32ea9c)


## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)

Internal changes:

- [x] **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)

## 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://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) 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://publish.obsidian.md/tasks-contributing)
- [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/docs/Queries/Explaining Queries.md b/docs/Queries/Explaining Queries.md
index bc8f0f24..96e9d100 100644
--- a/docs/Queries/Explaining Queries.md
+++ b/docs/Queries/Explaining Queries.md
@@ -114,7 +114,8 @@ Explanation of this Tasks code block query:
 
   (due before tomorrow) AND (is recurring) =>
     AND (All of):
-      due date is before 2022-10-22 (Saturday 22nd October 2022)
+      due before tomorrow =>
+        due date is before 2022-10-22 (Saturday 22nd October 2022)
       is recurring
 
   No grouping instructions supplied.
diff --git a/src/Query/Filter/BooleanField.ts b/src/Query/Filter/BooleanField.ts
index f5e47548..cd77ec1b 100644
--- a/src/Query/Filter/BooleanField.ts
+++ b/src/Query/Filter/BooleanField.ts
@@ -200,7 +200,15 @@ export class BooleanField extends Field {
             throw Error('null token value'); // This should not happen
         }
         const filter = this.subFields[token.value.trim()];
-        explanationStack.push(filter.explanation);
+        const explanation = this.simulateExplainFilter(filter);
+        explanationStack.push(explanation);
+    }
+
+    private simulateExplainFilter(filter: Filter) {
+        // In our caller, the explanationStack keeps the explanations but not the instruction lines.
+        // So to replicate the logic in Filter.explainFilterIndented(), we may need to add the
+        // instruction to the explanation.
+        return filter.simulateExplainFilter();
     }
 
     private explainOperator(token: Token, explanationStack: Explanation[]) {
diff --git a/src/Query/Filter/Filter.ts b/src/Query/Filter/Filter.ts
index 61dfe0ad..fd73e651 100644
--- a/src/Query/Filter/Filter.ts
+++ b/src/Query/Filter/Filter.ts
@@ -1,5 +1,5 @@
 import type { Task } from '../../Task/Task';
-import type { Explanation } from '../Explain/Explanation';
+import { Explanation } from '../Explain/Explanation';
 import type { SearchInfo } from '../SearchInfo';
 import { Statement } from '../Statement';
 
@@ -64,4 +64,13 @@ export class Filter {
             return `${explainedStatement} =>\n${explanation.asString(indent + '  ')}\n`;
         }
     }
+
+    public simulateExplainFilter() {
+        const needToShowInstruction = this.instruction !== this.explanation.asString();
+        if (needToShowInstruction) {
+            return new Explanation(this.instruction + ' =>', [this.explanation]);
+        } else {
+            return this.explanation;
+        }
+    }
 }


Test patch:
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
index 9afa33ea..ecd2387c 100644
--- a/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean_combinations.approved.explanation.text
+++ b/tests/Query/Explain/DocsSamplesForExplain.test.explain_boolean_combinations.approved.explanation.text
@@ -4,7 +4,8 @@ Explanation of this Tasks code block query:
 
   (due before tomorrow) AND (is recurring) =>
     AND (All of):
-      due date is before 2022-10-22 (Saturday 22nd October 2022)
+      due before tomorrow =>
+        due date is before 2022-10-22 (Saturday 22nd October 2022)
       is recurring
 
   No grouping instructions supplied.
diff --git a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt
index 76630e96..12806090 100644
--- a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt
+++ b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_explain.approved.txt
@@ -219,7 +219,8 @@ Result:
   (description includes d1) AND (priority medium) =>
     AND (All of):
       description includes d1
-      priority is medium
+      priority medium =>
+        priority is medium
 
 
 --------------------------------------------------------
@@ -247,7 +248,8 @@ Result:
     AND (All of):
       description includes d1
       NOT:
-        priority is medium
+        priority medium =>
+          priority is medium
 
 
 --------------------------------------------------------
@@ -274,7 +276,8 @@ Result:
     OR (At least one of):
       description includes d1
       description includes d2
-      priority is medium
+      priority medium =>
+        priority is medium
 
 
 --------------------------------------------------------
@@ -287,7 +290,8 @@ Result:
   (description includes d1) OR (priority medium) =>
     OR (At least one of):
       description includes d1
-      priority is medium
+      priority medium =>
+        priority is medium
 
 
 --------------------------------------------------------
@@ -315,7 +319,8 @@ Result:
     OR (At least one of):
       description includes d1
       NOT:
-        priority is medium
+        priority medium =>
+          priority is medium
 
 
 --------------------------------------------------------
@@ -341,7 +346,8 @@ Result:
   (description includes d1) XOR (priority medium) =>
     XOR (Exactly one of):
       description includes d1
-      priority is medium
+      priority medium =>
+        priority is medium
 
 
 --------------------------------------------------------
diff --git a/tests/Query/Filter/BooleanField.test.ts b/tests/Query/Filter/BooleanField.test.ts
index 80f20db0..0acc9326 100644
--- a/tests/Query/Filter/BooleanField.test.ts
+++ b/tests/Query/Filter/BooleanField.test.ts
@@ -220,7 +220,7 @@ describe('boolean query - explain', () => {
         return new Explainer(indentation).explainFilters(query1);
     }
 
-    it.failing('should make multi-line explanations consistent in and out of Boolean filter - issue #2707', () => {
+    it('should make multi-line explanations consistent in and out of Boolean filter - issue #2707', () => {
         // See https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2707
         const filter = 'description regex matches /buy/i';
 
@@ -231,7 +231,6 @@ describe('boolean query - explain', () => {
         const filter2Explanation = explainFilters(0, filter2);
 
         // Ensure that the full standalone explanation of filter1 is present in its explanation in the Boolean query:
-        // TODO: Make this test pass:
         expect(filter2Explanation).toContain(filter1Explanation);
 
         // These inline snapshots are provided for ease of visualising the behaviour:
@@ -241,12 +240,12 @@ describe('boolean query - explain', () => {
             "
         `);
 
-        // TODO: Include the original regex source line in this string:
         expect(filter2Explanation).toMatchInlineSnapshot(`
             "( description regex matches /buy/i ) AND ( path includes {{query.file.path}} ) =>
             ( description regex matches /buy/i ) AND ( path includes some/sample/note.md ) =>
               AND (All of):
-                using regex:            'buy' with flag 'i'
+                description regex matches /buy/i =>
+                  using regex:            'buy' with flag 'i'
                 path includes some/sample/note.md
             "
         `);