Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1542 (2023-01-16 10:04:58)

feat: update 'done' and 'not done' for new status types
<!--- Provide a general summary of your changes in the Title above -->

# Description

- `NON_TASK` no longer matches either `done` or `not done`
- `IN_PROGRESS` now matches `not done` instead of `done`
- `done` now matches `IN_PROGRESS` and `IN_PROGRESS `
- `not done` now matches `IN_PROGRESS` but does not match `NON_TASK`

Also, multiple documentation updates in this area.

## Breaking change???

I agonised for ages over whether to call this a breaking change.

Technically it is, as the results of `done` and `not done` change.

However, on reflection I genuinely feel that it is an improvement, and in-progress tasks will show up in the most common search, `not done`, which they did not previously.

In the unlikely event that users are concerned about this, I have documented how to restore the original behaviour by customising task statuses.

## Motivation and Context

To smooth the adoption of custom statuses, so users can control which statuses are matched by `done` and `not done`.

## How has this been tested?

- In automated tests
- Manually in my main vault

## Screenshots (if appropriate)

Before and after truth tables:

![image](https://user-images.githubusercontent.com/4840096/212640808-529b6a25-4c50-40f1-aa90-79643d70ee93.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:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)

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://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/docs/getting-started/statuses.md b/docs/getting-started/statuses.md
index 7d317034..13436611 100644
--- a/docs/getting-started/statuses.md
+++ b/docs/getting-started/statuses.md
@@ -46,8 +46,8 @@ The tasks shown are purely examples for context. The `~` column is just an arbit
 | Operation | TODO | IN_PROGRESS | DONE | CANCELLED | NON_TASK |
 | ----- | ----- | ----- | ----- | ----- | ----- |
 | Example Task | `- [ ] demo` | `- [/] demo` | `- [x] demo` | `- [-] demo` | `- [~] demo` |
-| Matches `done` | no | YES | YES | YES | YES |
-| Matches `not done` | YES | no | no | no | no |
+| Matches `done` | no | no | YES | YES | no |
+| Matches `not done` | YES | YES | no | no | no |
 | Matches `status.name includes todo` | YES | no | no | no | no |
 | Matches `status.name includes in progress` | no | YES | no | no | no |
 | Matches `status.name includes done` | no | no | YES | no | no |
@@ -57,7 +57,7 @@ The tasks shown are purely examples for context. The `~` column is just an arbit
 <!-- placeholder to force blank line after table --> <!-- endInclude -->
 
 {: .warning }
-The results of the above table are subject to change, as the `done` and `not done` filters have not yet been updated for the addition of the status types.
+The `group by status` results of the above table are subject to change.
 
 ## Standard Markdown task statuses
 
@@ -195,8 +195,8 @@ We are tracking this in [issue #1486](https://github.com/obsidian-tasks-group/ob
 
 ### Related searches
 
-- `done` - matches tasks with anything except space as the status symbol
-- `not done` - matches task with a space as the status symbol
+- `done` - matches tasks status types `TODO` and `CANCELLED`
+- `not done` - matches tasks with status types `TODO` and `IN_PROGRESS`
 - `status.name` text search
 - `sort by status.name`
 
@@ -214,26 +214,10 @@ We envisage adding `status.symbol`.
 
 ## Credit: Sytone and the 'Tasks SQL Powered' plugin
 
-This plugin's implementation of reading, searching and editing custom statuses was entirely made possible by the work of [Sytone](https://github.com/sytone) and his fork of Tasks called ['Tasks SQL Powered'](https://github.com/sytone/obsidian-tasks-x).
+This plugin's implementation of reading, searching and editing custom statuses was entirely made possible by the work of [Sytone](https://github.com/sytone) and his fork of Tasks called ['Tasks SQL Powered'](https://github.com/sytone/obsidian-tasks-x). [^task-x-version]
 
-When code in Tasks has been copied from 'Tasks SQL Powered', Sytone has been specifically credited as a co-author, that is, joint author, and these commits can be seen on the GitHub site: [Commits "Co-Authored-By: Sytone"](https://github.com/search?q=repo%3Aobsidian-tasks-group%2Fobsidian-tasks+%22Co-Authored-By%3A+Sytone%22&type=commits).
+Where code in Tasks has been copied from 'Tasks SQL Powered', Sytone has been specifically credited as a co-author, that is, joint author, and these commits can be seen on the GitHub site: [Commits "Co-Authored-By: Sytone"](https://github.com/search?q=repo%3Aobsidian-tasks-group%2Fobsidian-tasks+%22Co-Authored-By%3A+Sytone%22&type=commits).
 
-### Differences between 'Tasks' and 'Tasks SQL Powered'
-
-These two plugins are developed with justifiably different requirements:
-
-- As a released plugin, a priority of 'Tasks' is ensuring **backwards compatibility** with previous plugin releases, to avoid changing any (non-broken) behaviour of users' existing searches.
-- As an independent fork of 'Tasks', 'Tasks SQL Powered' is free to make changes to existing Tasks behaviour that could well be seen by users as improvements, and is **not required to maintain backwards compatibility** with 'Tasks'.
-
-The notes below may help anyone who is switching their vaults between the 'Tasks' and 'Tasks SQL Powered' to update their tasks or their queries to retain their intended results.
-
-They describe the changes made when back-porting the 'Tasks SQL Powered' custom status code, to retain backwards compatibility.
-
-| Topic                                                 | 'Tasks SQL Powered' [^task-x-version]                                                                                                                                                                                                                  | 'Tasks'                                                                                                        |
-| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
-| `done`, `not done`                                    | Only tasks with status characters `x` and `X` are treated as `done`.                                                                                                                                                                                   | All tasks with status characters other than `space` are treated as `done`  - for backwards compatibility.      |
-| `sort by status`                                      | Case-sensitive sort of the status character, in reverse alphabetical order. Unrecognised status characters are treated as empty, and appear before all other statuses.                                                                                 | All tasks with `space` status character sort first, then all other tasks  - for backwards compatibility.       |
-| `group by status`                                     | Groups by the name of the status, for example `Cancelled`, `Done`, `In Progress`, `EMPTY`.                                                                                                                                                             | Groups by either `Todo` if the status character is a space, or otherwise `Done` - for backwards compatibility. |
-| Handling unknown status characters, for example `[%]` | Stores and displays the task with no status character:<br>- this affects any custom CSS styling,<br>- toggling changes the line to `[]` - with no status character.<br>(The fix is to ensure all status characters are added in the plugin's settings) | Retains any unknown status characters:<br> - styling and toggling are unchanged                                |
+Subsequently, the custom statuses implementation in Tasks has diverged from the 'Tasks SQL Powered' significantly. However, none of the new features and fixes would have been possible without Sytone's foundation work, for which we are very grateful.
 
 [^task-x-version]: 'Tasks SQL Powered' as of [revision 2c0b659](https://github.com/sytone/obsidian-tasks-x/tree/2c0b659457cc80806ff18585c955496c76861b87) on 2 August 2022
diff --git a/docs/how-to/set-up-custom-statuses.md b/docs/how-to/set-up-custom-statuses.md
index c477dcbc..eab9a0ff 100644
--- a/docs/how-to/set-up-custom-statuses.md
+++ b/docs/how-to/set-up-custom-statuses.md
@@ -97,16 +97,16 @@ Suppose that you wanted to create a set of 3 statuses that cycle between each ot
 1. Repeat for the other two statuses and you should see:
     - ![After adding the other two new statuses](../images/settings-custom-statuses-important-loop-added.png)
 
-{: .warning }
-At the moment the error-checking in the status edit modal is not quite working, and if there is an invalid value such as an empty status symbol, the field can disappear completely. If this happens, the workaround is to close the modal, and click on the pencil to re-open it. We are tracking this as [issue #1498](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1498).
+{: .info }
+> The status changes are applied to newly edited tasks and subsequently opened notes immediately.
+>
+> You do not need to restart Obsidian, unless you want to make Tasks re-read all the tasks in your vault, for example so that searches are aware of the changed statuses.
 
 {: .warning }
 Tasks currently allows creation of more than one status with the same symbol. It silently ignores any duplicate symbols: only the first will be used. If in doubt, examine the available statuses in the status dropdown in the [‘Create or edit Task’ Modal]({{ site.baseurl }}{% link getting-started/create-or-edit-task.md %}).
 
 ## Test the new statuses
 
-The status changes are applied immediately. You do not need to restart Obsidian.
-
 Now you can create use the [‘Create or edit Task’ Modal]({{ site.baseurl }}{% link getting-started/create-or-edit-task.md %}) to create a new task and set its status:
 
 ![Task edit modal shows new statuses immediately](../images/modal-showing-new-statuses.png)
diff --git a/docs/queries/filters.md b/docs/queries/filters.md
index 6b9984e5..8d70b501 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -191,8 +191,17 @@ because the tasks starts before tomorrow. Only one of the dates needs to match.
 
 ### Status
 
-- `done` - matches tasks with anything except space as the status symbol
-- `not done` - matches task with a space as the status symbol
+- `done` - matches tasks status types `DONE` and `CANCELLED`
+- `not done` - matches status types with type `TODO` and `IN_PROGRESS`
+- Note: tasks with status type `NON_TASK` match neither filter.
+
+{: .info }
+> Prior to Tasks X.Y.Z, there was no concept of task status type, and so only the status symbol was used:
+>
+> - a task with `[ ]` used to count as `not done`
+> - any other character than space used to count as `done`
+>
+> The new behaviour is more flexible and was required to introduce support for in-progress and cancelled tasks. If the original behaviour is preferred, you can change the status types of every symbol except `space` to `DONE`. See [How to set up your custom statuses]({{ site.baseurl }}{% link how-to/set-up-custom-statuses.md %}).
 
 ### Status Name
 
diff --git a/src/Query/Filter/StatusField.ts b/src/Query/Filter/StatusField.ts
index 3ebc84a0..eff7806f 100644
--- a/src/Query/Filter/StatusField.ts
+++ b/src/Query/Filter/StatusField.ts
@@ -1,16 +1,29 @@
 import type { Task } from '../../Task';
 import type { Comparator } from '../Sorter';
+import { StatusType } from '../../StatusConfiguration';
 import { FilterInstructionsBasedField } from './FilterInstructionsBasedField';
 
 export class StatusField extends FilterInstructionsBasedField {
     constructor() {
         super();
 
-        // Backwards-compatibility note: In Tasks 1.22.0 and earlier, all tasks
+        // Backwards-compatibility change: In Tasks 1.22.0 and earlier, all tasks
         // with any status character except space were considered by the status filter
         // instructions to be done.
-        this._filters.add('done', (task: Task) => task.status.indicator !== ' ');
-        this._filters.add('not done', (task: Task) => task.status.indicator === ' ');
+        // In later versions:
+        //   StatusType.DONE counts as done
+        //   StatusType.CANCELLED counts as done
+        //   StatusType.TODO counts as not done
+        //   StatusType.IN_PROGRESS counts as not done
+        //   StatusType.NON_TASK does not match either
+        this._filters.add(
+            'done',
+            (task: Task) => task.status.type === StatusType.DONE || task.status.type === StatusType.CANCELLED,
+        );
+        this._filters.add(
+            'not done',
+            (task: Task) => task.status.type === StatusType.TODO || task.status.type === StatusType.IN_PROGRESS,
+        );
     }
 
     public fieldName(): string {


Test patch:
diff --git a/tests/CustomMatchers/CustomMatchersForFilters.ts b/tests/CustomMatchers/CustomMatchersForFilters.ts
index f2552329..b3fcc79b 100644
--- a/tests/CustomMatchers/CustomMatchersForFilters.ts
+++ b/tests/CustomMatchers/CustomMatchersForFilters.ts
@@ -3,6 +3,8 @@ import type { Task } from '../../src/Task';
 import type { FilterOrErrorMessage } from '../../src/Query/Filter/Filter';
 import { fromLine } from '../TestHelpers';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
+import type { StatusConfiguration } from '../../src/StatusConfiguration';
+import { Status } from '../../src/Status';
 
 /**
  @summary
@@ -63,6 +65,7 @@ declare global {
             toMatchTaskFromLine(line: string): R;
             toMatchTaskWithHeading(heading: string | null): R;
             toMatchTaskWithPath(path: string): R;
+            toMatchTaskWithStatus(statusConfiguration: StatusConfiguration): R;
         }
 
         interface Expect {
@@ -72,6 +75,7 @@ declare global {
             toMatchTaskFromLine(line: string): any;
             toMatchTaskWithHeading(heading: string | null): any;
             toMatchTaskWithPath(path: string): any;
+            toMatchTaskWithStatus(statusConfiguration: StatusConfiguration): any;
         }
 
         interface InverseAsymmetricMatchers {
@@ -81,6 +85,7 @@ declare global {
             toMatchTaskFromLine(line: string): any;
             toMatchTaskWithHeading(heading: string | null): any;
             toMatchTaskWithPath(path: string): any;
+            toMatchTaskWithStatus(statusConfiguration: StatusConfiguration): any;
         }
     }
 }
@@ -157,3 +162,9 @@ export function toMatchTaskWithPath(filter: FilterOrErrorMessage, path: string)
     const task = builder.path(path).build();
     return toMatchTask(filter, task);
 }
+
+export function toMatchTaskWithStatus(filter: FilterOrErrorMessage, statusConfiguration: StatusConfiguration) {
+    const builder = new TaskBuilder();
+    const task = builder.status(new Status(statusConfiguration)).build();
+    return toMatchTask(filter, task);
+}
diff --git a/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md b/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
index 8311898a..33c7bb35 100644
--- a/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
+++ b/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
@@ -3,8 +3,8 @@
 | Operation | TODO | IN_PROGRESS | DONE | CANCELLED | NON_TASK |
 | ----- | ----- | ----- | ----- | ----- | ----- |
 | Example Task | `- [ ] demo` | `- [/] demo` | `- [x] demo` | `- [-] demo` | `- [~] demo` |
-| Matches `done` | no | YES | YES | YES | YES |
-| Matches `not done` | YES | no | no | no | no |
+| Matches `done` | no | no | YES | YES | no |
+| Matches `not done` | YES | YES | no | no | no |
 | Matches `status.name includes todo` | YES | no | no | no | no |
 | Matches `status.name includes in progress` | no | YES | no | no | no |
 | Matches `status.name includes done` | no | no | YES | no | no |
diff --git a/tests/Query/Filter/StatusField.test.ts b/tests/Query/Filter/StatusField.test.ts
index 64add231..68aa0705 100644
--- a/tests/Query/Filter/StatusField.test.ts
+++ b/tests/Query/Filter/StatusField.test.ts
@@ -1,6 +1,6 @@
 import { StatusField } from '../../../src/Query/Filter/StatusField';
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
-import { toMatchTaskFromLine } from '../../CustomMatchers/CustomMatchersForFilters';
+import { toMatchTaskFromLine, toMatchTaskWithStatus } from '../../CustomMatchers/CustomMatchersForFilters';
 import { Status } from '../../../src/Status';
 import * as TestHelpers from '../../TestHelpers';
 import {
@@ -8,9 +8,11 @@ import {
     expectTaskComparesBefore,
     expectTaskComparesEqual,
 } from '../../CustomMatchers/CustomMatchersForSorting';
+import { StatusConfiguration, StatusType } from '../../../src/StatusConfiguration';
 
 expect.extend({
     toMatchTaskFromLine,
+    toMatchTaskWithStatus,
 });
 
 describe('status', () => {
@@ -19,12 +21,12 @@ describe('status', () => {
         const filter = new StatusField().createFilterOrErrorMessage('done');
 
         // Assert
-        expect(filter).not.toMatchTaskFromLine('- [ ] X');
-        expect(filter).toMatchTaskFromLine('- [x] X');
-        expect(filter).toMatchTaskFromLine('- [X] X');
-        expect(filter).toMatchTaskFromLine('- [/] X');
-        expect(filter).toMatchTaskFromLine('- [-] X');
-        expect(filter).toMatchTaskFromLine('- [!] X');
+        expect(filter).not.toMatchTaskWithStatus(Status.makeTodo().configuration);
+        expect(filter).toMatchTaskWithStatus(Status.makeDone().configuration);
+        expect(filter).toMatchTaskWithStatus(new StatusConfiguration('X', 'Really Done', 'x', true, StatusType.DONE));
+        expect(filter).not.toMatchTaskWithStatus(Status.makeInProgress().configuration);
+        expect(filter).toMatchTaskWithStatus(Status.makeCancelled().configuration);
+        expect(filter).not.toMatchTaskWithStatus(new StatusConfiguration('!', 'Todo', 'x', true, StatusType.TODO)); // 'done' checks type.
     });
 
     it('not done', () => {
@@ -32,12 +34,14 @@ describe('status', () => {
         const filter = new StatusField().createFilterOrErrorMessage('not done');
 
         // Assert
-        expect(filter).toMatchTaskFromLine('- [ ] X');
-        expect(filter).not.toMatchTaskFromLine('- [x] X');
-        expect(filter).not.toMatchTaskFromLine('- [X] X');
-        expect(filter).not.toMatchTaskFromLine('- [/] X');
-        expect(filter).not.toMatchTaskFromLine('- [-] X');
-        expect(filter).not.toMatchTaskFromLine('- [!] X');
+        expect(filter).toMatchTaskWithStatus(Status.makeTodo().configuration);
+        expect(filter).not.toMatchTaskWithStatus(Status.makeDone().configuration);
+        expect(filter).not.toMatchTaskWithStatus(
+            new StatusConfiguration('X', 'Really Done', 'x', true, StatusType.DONE),
+        );
+        expect(filter).toMatchTaskWithStatus(Status.makeInProgress().configuration);
+        expect(filter).not.toMatchTaskWithStatus(Status.makeCancelled().configuration);
+        expect(filter).toMatchTaskWithStatus(new StatusConfiguration('!', 'Todo', 'x', true, StatusType.TODO)); // 'not done' type.
     });
 });