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

Issue:
limit - enable opt in to show total numbers
### ⚠️ Please check that this feature request hasn't been suggested before.

- [X] I searched previous [Ideas in Discussions](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/categories/ideas-any-new-feature-requests-go-in-issues-please?discussions_q=category%3A%22Ideas%3A+Any+New+Feature+Requests+go+in+Issues+please%22+-label%3A%22status%3A+released%22+-label%3Aduplicate+sort%3Atop) didn't find any similar feature requests.
- [X] I searched previous [Issues](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+enhancement%22+sort%3Acomments-desc) didn't find any similar feature requests.
- [X] I am only requesting a single feature. Multiple changes should be split into individual requests, with links between them.
- [X] I believe my requested feature will be generally applicable to other users, not just me: it is not uniquely tied to my personal workflow.

### 🔖 Feature description

you should add a display to limit that allow to view the total task when limit crops it

when i do something like 

```
``tasks
not done
due before today
limit 5
``
```

and for example i have 22 overdue tasks - the display show `5 tasks` 

### ✔️ Solution

instead of just seeing the number of tasks,

i'd like to see `5/22 tasks`

### ❓ Alternatives

instead of doing this by default ,

the display could also jut indicate `First 5 tasks (total unknown)`

and then a opt in like `limit 5 with stats` could enable the more expensive full counting

### 📝 Additional Context

...

Pull request:
feat: 'show task count' now shows total, if 'limit' applied
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

- If there was a limit applied in the search, the results now show the total number of tasks that were found.
- For example, `50 of 686 tasks` instead of `50 tasks`

The original suggestion was to make this an optional feature, but it is so useful, and the time-cost of adding optional features is significant, so I've enabled it automatically.

Also update the user docs.

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

Fixes #2294:

- #2294

## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

## Screenshots (if appropriate)

![image](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/7132cf87-eead-421a-b107-2392735118d4)


## 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/Queries/Limiting.md b/docs/Queries/Limiting.md
index ab9f5f77..6d90231d 100644
--- a/docs/Queries/Limiting.md
+++ b/docs/Queries/Limiting.md
@@ -27,3 +27,11 @@ Shorthand is `limit groups <number>`.
 
 > [!released]
 > `limit groups to <number> tasks` was introduced in Tasks 3.8.0.
+
+## Seeing the total number of tasks found
+
+If either `limit` option prevents any tasks from being displayed in the results, the total number number will be shown, for example:
+
+```text
+50 of 686 tasks
+```
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index 2af1273a..0b2db1d9 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -251,7 +251,7 @@ Problem line: "${line}"`;
                 taskGroups.applyTaskLimit(this._taskGroupLimit);
             }
 
-            return new QueryResult(taskGroups);
+            return new QueryResult(taskGroups, tasksSorted.length);
         } catch (e) {
             const description = 'Search failed';
             return QueryResult.fromError(errorMessageForException(description, e));
diff --git a/src/Query/QueryResult.ts b/src/Query/QueryResult.ts
index b0f36d6a..7f3e8881 100644
--- a/src/Query/QueryResult.ts
+++ b/src/Query/QueryResult.ts
@@ -1,12 +1,19 @@
 import { TaskGroups } from './TaskGroups';
 import type { TaskGroup } from './TaskGroup';
 
+function taskCountPluralised(tasksCount: number) {
+    return `task${tasksCount !== 1 ? 's' : ''}`;
+}
+
 export class QueryResult {
     public readonly taskGroups: TaskGroups;
+    public readonly totalTasksCountBeforeLimit: number = 0;
+
     private _searchErrorMessage: string | undefined = undefined;
 
-    constructor(groups: TaskGroups) {
+    constructor(groups: TaskGroups, totalTasksCountBeforeLimit: number) {
         this.taskGroups = groups;
+        this.totalTasksCountBeforeLimit = totalTasksCountBeforeLimit;
     }
 
     public get searchErrorMessage(): string | undefined {
@@ -23,7 +30,12 @@ export class QueryResult {
 
     public totalTasksCountDisplayText() {
         const tasksCount = this.totalTasksCount;
-        return `${tasksCount} task${tasksCount !== 1 ? 's' : ''}`;
+        const tasksCountBeforeLimit = this.totalTasksCountBeforeLimit;
+        if (tasksCount === tasksCountBeforeLimit) {
+            return `${tasksCount} ${taskCountPluralised(tasksCount)}`;
+        } else {
+            return `${tasksCount} of ${tasksCountBeforeLimit} ${taskCountPluralised(tasksCountBeforeLimit)}`;
+        }
     }
 
     public get groups(): TaskGroup[] {
@@ -31,7 +43,7 @@ export class QueryResult {
     }
 
     static fromError(message: string): QueryResult {
-        const result = new QueryResult(new TaskGroups([], []));
+        const result = new QueryResult(new TaskGroups([], []), 0);
         result._searchErrorMessage = message;
         return result;
     }


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 0bc64296..9d5698a3 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -1226,6 +1226,9 @@ At most 8 tasks per group (if any "group by" options are supplied).
 - [ ] Task 4 - will be sorted to 2nd place, so should pass limit
 `;
             expect('\n' + soleTaskGroup.tasksAsStringOfLines()).toStrictEqual(expectedTasks);
+
+            expect(queryResult.taskGroups.totalTasksCount()).toEqual(2);
+            expect(queryResult.totalTasksCountBeforeLimit).toEqual(6);
         });
 
         it('should apply group limit correctly, after sorting tasks', () => {
@@ -1270,6 +1273,9 @@ At most 8 tasks per group (if any "group by" options are supplied).
                 - [ ] Task 5 - will be sorted to 3nd place in the second group and pass the limit
                 "
             `);
+
+            expect(queryResult.taskGroups.totalTasksCount()).toEqual(5);
+            expect(queryResult.totalTasksCountBeforeLimit).toEqual(6);
         });
     });
 
diff --git a/tests/Query/QueryResult.test.ts b/tests/Query/QueryResult.test.ts
index 02962054..8381bf9c 100644
--- a/tests/Query/QueryResult.test.ts
+++ b/tests/Query/QueryResult.test.ts
@@ -6,9 +6,13 @@ import { fromLine } from '../TestHelpers';
 
 describe('QueryResult', () => {
     function createUngroupedQueryResult(tasks: Task[]) {
+        return createUngroupedQueryResultWithLimit(tasks, tasks.length);
+    }
+
+    function createUngroupedQueryResultWithLimit(tasks: Task[], totalTasksCountBeforeLimit: number) {
         const groupers: Grouper[] = [];
         const groups = new TaskGroups(groupers, tasks);
-        return new QueryResult(groups);
+        return new QueryResult(groups, totalTasksCountBeforeLimit);
     }
 
     it('should create a QueryResult from TaskGroups', () => {
@@ -18,7 +22,7 @@ describe('QueryResult', () => {
         const groups = new TaskGroups(groupers, tasks);
 
         // Act
-        const queryResult = new QueryResult(groups);
+        const queryResult = new QueryResult(groups, 0);
 
         // Assert
         expect(queryResult.totalTasksCount).toEqual(0);
@@ -37,6 +41,8 @@ describe('QueryResult', () => {
     });
 
     describe('Text representation of tasks count', () => {
+        // Simple cases - where no limit was applied
+
         it('should pluralise "tasks" if 0 matches', () => {
             const tasks: Task[] = [];
             const queryResult = createUngroupedQueryResult(tasks);
@@ -57,5 +63,28 @@ describe('QueryResult', () => {
             const queryResult = createUngroupedQueryResult(tasks);
             expect(queryResult.totalTasksCountDisplayText()).toEqual('2 tasks');
         });
+
+        // Cases where a limit was applied
+
+        it('should show original number of matching tasks if limit was applied', () => {
+            const tasks: Task[] = [];
+            const queryResult = createUngroupedQueryResultWithLimit(tasks, 1);
+            expect(queryResult.totalTasksCountDisplayText()).toEqual('0 of 1 task');
+        });
+
+        it('should show original number of matching tasks if limit was applied', () => {
+            const tasks = [fromLine({ line: '- [ ] Do something' })];
+            const queryResult = createUngroupedQueryResultWithLimit(tasks, 2);
+            expect(queryResult.totalTasksCountDisplayText()).toEqual('1 of 2 tasks');
+        });
+
+        it('should show original number of matching tasks if limit was applied', () => {
+            const tasks = [
+                fromLine({ line: '- [ ] Do something more complicated 1' }),
+                fromLine({ line: '- [ ] Do something more complicated 2' }),
+            ];
+            const queryResult = createUngroupedQueryResultWithLimit(tasks, 9);
+            expect(queryResult.totalTasksCountDisplayText()).toEqual('2 of 9 tasks');
+        });
     });
 });