Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2617 (2024-01-25 09:14:33)

feat: Expose `query.allTasks` in scripting
<!--- Provide a general summary of your changes in the Title above -->

# Description

- Add new query property `query.allTasks`
- Update documentation

## Motivation and Context

This will be needed for custom filters, sorting and grouping with task-dependency-related properties.

## How has this been tested?

- Added new test
- Exploratory testing

## Screenshots (if appropriate)

## 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)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)

Internal changes:

- [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/Introduction.md b/docs/Introduction.md
index 480007de..0986d7af 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -13,20 +13,22 @@ _In recent [releases](https://github.com/obsidian-tasks-group/obsidian-tasks/rel
     Move the older ones down to the top of the comment block below...
 -->
 
+- X.Y.Z:
+  - `query.allTasks` is now available in custom searches: see [[Query Properties#Values for Query Search Properties|query search properties]].
 - 6.0.0:
   - Add [[Custom Sorting|custom sorting]].
   - Document the [[Sorting#Default sort order|default sort order]].
   - **Warning**: This release contains some **bug-fixes** to **sorting** and to treatment of **invalid dates**.
     - The changes are detailed in [[breaking changes#Tasks 6.0.0 (19 January 2024)|breaking changes]], even though they are all improvements to the previous behaviour.
     - You may need to update any CSS snippets for the Edit or Postpone buttons: see [[How to style buttons]].
-- 5.6.0:
-  - The [[Postponing|postpone]] menu now offers `today` and `tomorrow`.
-- 5.5.0:
-  - The [[Create or edit Task]] modal can now edit Created, Done and Cancelled dates
-  - Add support for [[Dates#Cancelled date|cancelled dates]].
 
 > [!Released]- Earlier Releases
 >
+> - 5.6.0:
+>   - The [[Postponing|postpone]] menu now offers `today` and `tomorrow`.
+> - 5.5.0:
+>   - The [[Create or edit Task]] modal can now edit Created, Done and Cancelled dates
+>   - Add support for [[Dates#Cancelled date|cancelled dates]].
 > - 5.4.0:
 >   - Add [[Layout#Full Mode|'full mode']] to turn off `short mode`.
 >   - Add any [[Grouping|'group by']] and [[Sorting|'sort by']] instructions to [[Explaining Queries|explain]] output.
diff --git a/docs/Queries/Grouping.md b/docs/Queries/Grouping.md
index b87d33d8..be4de83c 100644
--- a/docs/Queries/Grouping.md
+++ b/docs/Queries/Grouping.md
@@ -206,7 +206,7 @@ Since Tasks X.Y.Z, **[[Custom Grouping|custom grouping]]  by Blocked By** is now
 group by function task.blockedBy
 ```
 
-- Group by the Ids of the tasks that this task depends on, if any.
+- Group by the Ids of the tasks that each task depends on, if any.
 - If a task depends on more than one other task, it will be listed multiple times.
 - Note that currently there is no way to access the tasks being depended on.
 
diff --git a/docs/Scripting/Query Properties.md b/docs/Scripting/Query Properties.md
index 2e48b8bf..4c159b07 100644
--- a/docs/Scripting/Query Properties.md
+++ b/docs/Scripting/Query Properties.md
@@ -42,3 +42,19 @@ This page documents all the available pieces of information in Queries that you
 1. The presence of `.md` filename extensions is chosen to match the existing conventions in the Tasks filter instructions [[Filters#File Path|path]] and [[Filters#File Name|filename]].
 1. `query.file.pathWithoutExtension` was added in Tasks 4.8.0.
 1. `query.file.filenameWithoutExtension` was added in Tasks 4.8.0.
+
+## Values for Query Search Properties
+
+<!-- placeholder to force blank line before included text --><!-- include: QueryProperties.test.query_search_properties.approved.md -->
+
+| Field | Type | Example |
+| ----- | ----- | ----- |
+| `query.allTasks` | `Task[]` | `[... an array with all the Tasks-tracked tasks in the vault ...]` |
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+1. `query.allTasks` provides access to all the tasks that Tasks has read from the vault.
+    - If [[Global Filter|global filter]] is enabled, only tasks containing the global filter are included.
+    - The [[Global Query|global query]] does not affect `query.allTasks`: all tasks tracked by the Tasks plugin are included.
+    - See [[Task Properties]] for the available properties on each task.
+    - `query.allTasks` was added in Tasks X.Y.Z.
diff --git a/src/Query/SearchInfo.ts b/src/Query/SearchInfo.ts
index d098910e..04e4a1b1 100644
--- a/src/Query/SearchInfo.ts
+++ b/src/Query/SearchInfo.ts
@@ -1,5 +1,5 @@
 import type { Task } from '../Task/Task';
-import { type QueryContext, makeQueryContext } from '../Scripting/QueryContext';
+import { type QueryContext, makeQueryContextWithTasks } from '../Scripting/QueryContext';
 
 /**
  * SearchInfo contains selected data passed in from the {@link Query} being executed.
@@ -30,6 +30,6 @@ export class SearchInfo {
      * @return A QueryContext, or undefined if the path to the query file is unknown.
      */
     public queryContext(): QueryContext | undefined {
-        return this.queryPath ? makeQueryContext(this.queryPath) : undefined;
+        return this.queryPath ? makeQueryContextWithTasks(this.queryPath, this.allTasks) : undefined;
     }
 }
diff --git a/src/Scripting/QueryContext.ts b/src/Scripting/QueryContext.ts
index 7542f466..41f022e7 100644
--- a/src/Scripting/QueryContext.ts
+++ b/src/Scripting/QueryContext.ts
@@ -1,3 +1,4 @@
+import type { Task } from '../Task/Task';
 import { TasksFile } from './TasksFile';
 
 /**
@@ -18,6 +19,7 @@ import { TasksFile } from './TasksFile';
 export interface QueryContext {
     query: {
         file: TasksFile;
+        allTasks: Readonly<Task[]>;
     };
 }
 
@@ -26,12 +28,18 @@ export interface QueryContext {
  * @param path
  *
  * @see SearchInfo.queryContext
+ * @see makeQueryContextWithTasks
  */
 export function makeQueryContext(path: string): QueryContext {
+    return makeQueryContextWithTasks(path, []);
+}
+
+export function makeQueryContextWithTasks(path: string, allTasks: Readonly<Task[]>): QueryContext {
     const tasksFile = new TasksFile(path);
     return {
         query: {
             file: tasksFile,
+            allTasks: allTasks,
         },
     };
 }


Test patch:
diff --git a/tests/Scripting/QueryContext.test.ts b/tests/Scripting/QueryContext.test.ts
index b46a6068..1c86ce64 100644
--- a/tests/Scripting/QueryContext.test.ts
+++ b/tests/Scripting/QueryContext.test.ts
@@ -4,35 +4,55 @@ import { FolderField } from '../../src/Query/Filter/FolderField';
 import { PathField } from '../../src/Query/Filter/PathField';
 import { RootField } from '../../src/Query/Filter/RootField';
 import { makeQueryContext } from '../../src/Scripting/QueryContext';
+import { FunctionField } from '../../src/Query/Filter/FunctionField';
+import { SearchInfo } from '../../src/Query/SearchInfo';
+
+const path = 'a/b/c.md';
+const task = new TaskBuilder().path(path).build();
+const queryContext = makeQueryContext(path);
 
 describe('QueryContext', () => {
     describe('values should all match their corresponding filters', () => {
-        const path = 'a/b/c.md';
-        const task = new TaskBuilder().path(path).build();
-        const queryContext = makeQueryContext(path);
-
-        it('root', () => {
+        it('query.file.root', () => {
             const instruction = `root includes ${queryContext.query.file.root}`;
             const filter = new RootField().createFilterOrErrorMessage(instruction);
             expect(filter).toMatchTask(task);
         });
 
-        it('path', () => {
+        it('query.file.path', () => {
             const instruction = `path includes ${queryContext.query.file.path}`;
             const filter = new PathField().createFilterOrErrorMessage(instruction);
             expect(filter).toMatchTask(task);
         });
 
-        it('folder', () => {
+        it('query.file.folder', () => {
             const instruction = `folder includes ${queryContext.query.file.folder}`;
             const filter = new FolderField().createFilterOrErrorMessage(instruction);
             expect(filter).toMatchTask(task);
         });
 
-        it('filename', () => {
+        it('query.file.filename', () => {
             const instruction = `filename includes ${queryContext.query.file.filename}`;
             const filter = new FilenameField().createFilterOrErrorMessage(instruction);
             expect(filter).toMatchTask(task);
         });
     });
+
+    describe('non-file properties', () => {
+        it('query.allTasks', () => {
+            // Arrange
+            // An artificial example, just to demonstrate that query.allTasks is accessible via scripting,
+            // when the SearchInfo parameter is converted to a QueryContext.
+            const instruction = 'group by function query.allTasks.length';
+            const grouper = new FunctionField().createGrouperFromLine(instruction);
+            expect(grouper).not.toBeNull();
+            const searchInfo = new SearchInfo(path, [task]);
+
+            // Act
+            const group: string[] = grouper!.grouper(task, searchInfo);
+
+            // Assert
+            expect(group).toEqual(['1']);
+        });
+    });
 });
diff --git a/tests/Scripting/QueryProperties.test.query_search_properties.approved.md b/tests/Scripting/QueryProperties.test.query_search_properties.approved.md
new file mode 100644
index 00000000..6af5b89e
--- /dev/null
+++ b/tests/Scripting/QueryProperties.test.query_search_properties.approved.md
@@ -0,0 +1,8 @@
+<!-- placeholder to force blank line before included text -->
+
+| Field | Type | Example |
+| ----- | ----- | ----- |
+| `query.allTasks` | `Task[]` | `[... an array with all the Tasks-tracked tasks in the vault ...]` |
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/QueryProperties.test.ts b/tests/Scripting/QueryProperties.test.ts
index ccb44f61..f7bdd384 100644
--- a/tests/Scripting/QueryProperties.test.ts
+++ b/tests/Scripting/QueryProperties.test.ts
@@ -1,17 +1,21 @@
-import { expandPlaceholders } from '../../src/Scripting/ExpandPlaceholders';
-import { makeQueryContext } from '../../src/Scripting/QueryContext';
+import { makeQueryContextWithTasks } from '../../src/Scripting/QueryContext';
 
 import { verifyMarkdownForDocs } from '../TestingTools/VerifyMarkdown';
 import { MarkdownTable } from '../../src/lib/MarkdownTable';
+import { parseAndEvaluateExpression } from '../../src/Scripting/TaskExpression';
+import { TaskBuilder } from '../TestingTools/TaskBuilder';
 import { addBackticks, determineExpressionType, formatToRepresentType } from './ScriptingTestHelpers';
 
 describe('query', () => {
     function verifyFieldDataForReferenceDocs(fields: string[]) {
         const markdownTable = new MarkdownTable(['Field', 'Type', 'Example']);
         const path = 'root/sub-folder/file containing query.md';
-        const queryContext = makeQueryContext(path);
+        const task = new TaskBuilder()
+            .description('... an array with all the Tasks-tracked tasks in the vault ...')
+            .build();
+        const queryContext = makeQueryContextWithTasks(path, [task]);
         for (const field of fields) {
-            const value1 = expandPlaceholders('{{' + field + '}}', queryContext);
+            const value1 = parseAndEvaluateExpression(task, field, queryContext);
             const cells = [
                 addBackticks(field),
                 addBackticks(determineExpressionType(value1)),
@@ -32,4 +36,8 @@ describe('query', () => {
             'query.file.filenameWithoutExtension',
         ]);
     });
+
+    it('search properties', () => {
+        verifyFieldDataForReferenceDocs(['query.allTasks']);
+    });
 });
diff --git a/tests/Scripting/ScriptingTestHelpers.ts b/tests/Scripting/ScriptingTestHelpers.ts
index 87d41b63..6203a5f4 100644
--- a/tests/Scripting/ScriptingTestHelpers.ts
+++ b/tests/Scripting/ScriptingTestHelpers.ts
@@ -1,6 +1,7 @@
 import moment from 'moment';
 import { TasksDate } from '../../src/Scripting/TasksDate';
 import { TaskRegularExpressions } from '../../src/Task/TaskRegularExpressions';
+import { Task } from '../../src/Task/Task';
 
 export function formatToRepresentType(x: any): string {
     if (typeof x === 'string') {
@@ -11,6 +12,10 @@ export function formatToRepresentType(x: any): string {
         return `moment('${x.format(TaskRegularExpressions.dateTimeFormat)}')`;
     }
 
+    if (x instanceof Task) {
+        return x.description;
+    }
+
     if (x instanceof TasksDate) {
         return x.formatAsDateAndTime();
     }
@@ -33,7 +38,7 @@ export function addBackticks(x: any) {
     return quotedText;
 }
 
-export function determineExpressionType(value: any) {
+export function determineExpressionType(value: any): string {
     if (value === null) {
         return 'null';
     }
@@ -42,13 +47,17 @@ export function determineExpressionType(value: any) {
         return 'Moment';
     }
 
+    if (value instanceof Task) {
+        return 'Task';
+    }
+
     if (value instanceof TasksDate) {
         return 'TasksDate';
     }
 
     if (Array.isArray(value)) {
         if (value.length > 0) {
-            return `${typeof value[0]}[]`;
+            return `${determineExpressionType(value[0])}[]`;
         } else {
             return 'any[]';
         }