Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3128 (2024-10-18 21:52:53)

feat: Add experimental 'show/hide tree' instruction - disabled by default
# 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] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
  - Issue/discussion: #60
- [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:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Description

_**Warning: These instructions may be removed or renamed, and are not (yet) intended for end-user use.**_

Add two new **experimental** - and undocumented instructions:

```
show tree
hide tree
```

`show tree ` causes Tasks to render nested tasks and list items, and is off by default.

I am not sure whether `hide tree` is really the right vocabulary for this long-term, but it is definitely better than `hide children` which was the first thing I tried.

## Motivation and Context

More work on:

- #60

This will allow me to continue making Tasks releases whilst we improve the rendering of nested lines.


## How has this been tested?

- Automated tests
- Exploratory 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.
- [ ] 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 follows 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/resources/sample_vaults/Tasks-Demo/Other Plugins/Dataview/Parent-Child relationships - Searches - Tasks.md b/resources/sample_vaults/Tasks-Demo/Other Plugins/Dataview/Parent-Child relationships - Searches - Tasks.md
index 6da1ab4f..9450ade9 100644
--- a/resources/sample_vaults/Tasks-Demo/Other Plugins/Dataview/Parent-Child relationships - Searches - Tasks.md
+++ b/resources/sample_vaults/Tasks-Demo/Other Plugins/Dataview/Parent-Child relationships - Searches - Tasks.md
@@ -8,6 +8,7 @@
 folder includes {{query.file.folder}}
 sort by function task.lineNumber
 hide backlinks
+show tree
 ```
 
 Note: Tasks uses the due date in its default sort order,<br>so we override that by sorting by line number.
@@ -20,6 +21,7 @@ Note: Tasks uses the due date in its default sort order,<br>so we override that
 folder includes {{query.file.folder}}
 done
 hide backlinks
+show tree
 ```
 
 ### 2.2 Fully completed
@@ -31,6 +33,7 @@ folder includes {{query.file.folder}}
 # There is no Tasks instruction for "fully done" yet
 done
 hide backlinks
+show tree
 ```
 
 ### 2.3 Not completed
@@ -39,6 +42,7 @@ hide backlinks
 folder includes {{query.file.folder}}
 not done
 hide backlinks
+show tree
 ```
 
 ### 2.4 Not fully completed
@@ -50,6 +54,7 @@ folder includes {{query.file.folder}}
 # There is no Tasks instruction for "not fully done" yet
 not done
 hide backlinks
+show tree
 ```
 
 ## 3 Sorting
@@ -62,6 +67,7 @@ hide backlinks
 folder includes {{query.file.folder}}
 sort by function task.lineNumber
 hide backlinks
+show tree
 ```
 
 ### 3.2 Reverse Order
@@ -70,6 +76,7 @@ hide backlinks
 folder includes {{query.file.folder}}
 sort by function reverse task.lineNumber
 hide backlinks
+show tree
 ```
 
 ## 4 Grouping
@@ -83,6 +90,7 @@ folder includes {{query.file.folder}}
 done
 group by heading
 hide backlinks
+show tree
 ```
 
 ### 4.2 Fully completed
@@ -95,6 +103,7 @@ folder includes {{query.file.folder}}
 done
 group by heading
 hide backlinks
+show tree
 ```
 
 ### 4.3 Not completed
@@ -104,6 +113,7 @@ folder includes {{query.file.folder}}
 not done
 group by heading
 hide backlinks
+show tree
 ```
 
 ### 4.4 Not fully completed
@@ -116,4 +126,5 @@ folder includes {{query.file.folder}}
 not done
 group by heading
 hide backlinks
+show tree
 ```
diff --git a/src/Layout/QueryLayoutOptions.ts b/src/Layout/QueryLayoutOptions.ts
index 7eb5711b..fc1add1b 100644
--- a/src/Layout/QueryLayoutOptions.ts
+++ b/src/Layout/QueryLayoutOptions.ts
@@ -9,6 +9,7 @@ export class QueryLayoutOptions {
     hideBacklinks: boolean = false;
     hideEditButton: boolean = false;
     hideUrgency: boolean = true;
+    hideTree: boolean = true; // WARNING: undocumented, and not yet ready for release.
     shortMode: boolean = false;
     explainQuery: boolean = false;
 }
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index ef1d4178..c0a0aa23 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -36,7 +36,7 @@ export class Query implements IQuery {
     private _ignoreGlobalQuery: boolean = false;
 
     private readonly hideOptionsRegexp =
-        /^(hide|show) (task count|backlink|priority|cancelled date|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|postpone button|urgency|tags|depends on|id|on completion)/i;
+        /^(hide|show) (task count|backlink|priority|cancelled date|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|postpone button|urgency|tags|depends on|id|on completion|tree)/i;
     private readonly shortModeRegexp = /^short/i;
     private readonly fullModeRegexp = /^full/i;
     private readonly explainQueryRegexp = /^explain/i;
@@ -309,6 +309,10 @@ ${statement.explainStatement('    ')}
             const option = hideOptionsMatch[2].toLowerCase();
 
             switch (option) {
+                case 'tree':
+                    // WARNING: undocumented, and not yet ready for release.
+                    this._queryLayoutOptions.hideTree = hide;
+                    break;
                 case 'task count':
                     this._queryLayoutOptions.hideTaskCount = hide;
                     break;
diff --git a/src/Renderer/QueryResultsRenderer.ts b/src/Renderer/QueryResultsRenderer.ts
index 760d75d1..35c9188c 100644
--- a/src/Renderer/QueryResultsRenderer.ts
+++ b/src/Renderer/QueryResultsRenderer.ts
@@ -220,28 +220,36 @@ export class QueryResultsRenderer {
         });
 
         for (const [taskIndex, task] of tasks.entries()) {
-            if (this.alreadyRendered(task, renderedTasks)) {
-                continue;
-            }
-
-            if (this.willBeRenderedLater(task, renderedTasks, tasks)) {
-                continue;
-            }
-
-            const listItem = await this.addTaskOrListItem(
-                taskList,
-                taskLineRenderer,
-                task,
-                taskIndex,
-                queryRendererParameters,
-            );
-            renderedTasks.add(task);
-
-            if (task.children.length > 0) {
-                await this.createTaskList(task.children, listItem, queryRendererParameters, renderedTasks);
-                task.children.forEach((childTask) => {
-                    renderedTasks.add(childTask);
-                });
+            if (this.query.queryLayoutOptions.hideTree) {
+                /* Old-style rendering of tasks:
+                 *  - What is rendered:
+                 *      - Only task lines that match the query are rendered, as a flat list
+                 *  - The order that lines are rendered:
+                 *      - Tasks are rendered in the order specified in 'sort by' instructions and default sort order.
+                 */
+                if (task instanceof Task) {
+                    await this.addTask(taskList, taskLineRenderer, task, taskIndex, queryRendererParameters);
+                }
+            } else {
+                /* New-style rendering of tasks:
+                 *  - What is rendered:
+                 *      - Task lines that match the query are rendered, as a tree.
+                 *      - Currently, all child tasks and list items of the found tasks are shown,
+                 *        including any child tasks that did not match the query.
+                 *  - The order that lines are rendered:
+                 *      - The top-level/outermost tasks are sorted in the order specified in 'sort by'
+                 *        instructions and default sort order.
+                 *      - Child tasks (and list items) are shown in their original order in their Markdown file.
+                 */
+                await this.addTaskOrListItemAndChildren(
+                    taskList,
+                    taskLineRenderer,
+                    task,
+                    taskIndex,
+                    queryRendererParameters,
+                    tasks,
+                    renderedTasks,
+                );
             }
         }
 
@@ -274,6 +282,40 @@ export class QueryResultsRenderer {
         return renderedTasks.has(task);
     }
 
+    private async addTaskOrListItemAndChildren(
+        taskList: HTMLUListElement,
+        taskLineRenderer: TaskLineRenderer,
+        task: ListItem,
+        taskIndex: number,
+        queryRendererParameters: QueryRendererParameters,
+        tasks: ListItem[],
+        renderedTasks: Set<ListItem>,
+    ) {
+        if (this.alreadyRendered(task, renderedTasks)) {
+            return;
+        }
+
+        if (this.willBeRenderedLater(task, renderedTasks, tasks)) {
+            return;
+        }
+
+        const listItem = await this.addTaskOrListItem(
+            taskList,
+            taskLineRenderer,
+            task,
+            taskIndex,
+            queryRendererParameters,
+        );
+        renderedTasks.add(task);
+
+        if (task.children.length > 0) {
+            await this.createTaskList(task.children, listItem, queryRendererParameters, renderedTasks);
+            task.children.forEach((childTask) => {
+                renderedTasks.add(childTask);
+            });
+        }
+    }
+
     private async addTaskOrListItem(
         taskList: HTMLUListElement,
         taskLineRenderer: TaskLineRenderer,


Test patch:
diff --git a/tests/Query/Query.test.ts b/tests/Query/Query.test.ts
index be915775..741cf933 100644
--- a/tests/Query/Query.test.ts
+++ b/tests/Query/Query.test.ts
@@ -466,6 +466,7 @@ describe('Query parsing', () => {
             'hide start date',
             'hide tags',
             'hide task count',
+            'hide tree',
             'hide urgency',
             'ignore global query',
             'limit 42',
@@ -489,6 +490,7 @@ describe('Query parsing', () => {
             'show start date',
             'show tags',
             'show task count',
+            'show tree',
             'show urgency',
         ];
         test.concurrent.each<string>(filters)('recognises %j', (filter) => {
@@ -1302,6 +1304,11 @@ describe('Query', () => {
             const query = new Query('hide on completion');
             expect(query.taskLayoutOptions.isShown(TaskLayoutComponent.OnCompletion)).toEqual(false);
         });
+
+        it('should hide "tree" by default', () => {
+            const query = new Query('');
+            expect(query.queryLayoutOptions.hideTree).toEqual(true);
+        });
     });
 
     describe('sorting', () => {
diff --git a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items_hidden.approved.html b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items_hidden.approved.html
new file mode 100644
index 00000000..78627a4a
--- /dev/null
+++ b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items_hidden.approved.html
@@ -0,0 +1,125 @@
+<div>
+  <ul class="contains-task-list plugin-tasks-query-result tasks-layout-hide-urgency">
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="0"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="0" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>parent 1</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="1"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="1" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>child 1</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="2"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="2" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>grandchild 1</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="3"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="3" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>child 2</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="4"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="4" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>grand grand child</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="5"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="5" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>parent 2</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+  </ul>
+  <div class="task-count">6 tasks</div>
+</div>
diff --git a/tests/Renderer/QueryResultsRenderer.test.ts b/tests/Renderer/QueryResultsRenderer.test.ts
index e3f7e89a..50490a9e 100644
--- a/tests/Renderer/QueryResultsRenderer.test.ts
+++ b/tests/Renderer/QueryResultsRenderer.test.ts
@@ -55,19 +55,27 @@ describe('QueryResultsRenderer tests', () => {
         await verifyRenderedTasksHTML(allTasks);
     });
 
+    const showTree = 'show tree\n';
+    const hideTree = 'hide tree\n';
+
+    it('parent-child items hidden', async () => {
+        const allTasks = readTasksFromSimulatedFile(inheritance_rendering_sample);
+        await verifyRenderedTasksHTML(allTasks, hideTree + 'sort by function task.lineNumber');
+    });
+
     it('parent-child items', async () => {
         const allTasks = readTasksFromSimulatedFile(inheritance_rendering_sample);
-        await verifyRenderedTasksHTML(allTasks, 'sort by function task.lineNumber');
+        await verifyRenderedTasksHTML(allTasks, showTree + 'sort by function task.lineNumber');
     });
 
     it('parent-child items reverse sorted', async () => {
         const allTasks = readTasksFromSimulatedFile(inheritance_rendering_sample);
-        await verifyRenderedTasksHTML(allTasks, 'sort by function reverse task.lineNumber');
+        await verifyRenderedTasksHTML(allTasks, showTree + 'sort by function reverse task.lineNumber');
     });
 
     it('should render tasks without their parents', async () => {
         // example chosen to match subtasks whose parents do not match the query
         const allTasks = readTasksFromSimulatedFile(inheritance_task_2listitem_3task);
-        await verifyRenderedTasksHTML(allTasks, 'description includes grandchild');
+        await verifyRenderedTasksHTML(allTasks, showTree + 'description includes grandchild');
     });
 });