Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3016 (2024-08-07 22:38:22)

feat: Add 'keep' option to 'On Completion'
# 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: #1647
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [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

Previous references to 'ignore' in the On Completion docs were confusion - see the warning I have now deleted.

This now adds an explicit 'keep' option, which behaves the same as no 'On Completion' option having been provided.



## 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. -->

It basically enables the documentation to be honest and accurate now.

## How has this been tested?

- New unit tests
- Exploratory testing in the test vault


## 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 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/docs/Getting Started/On Completion.md b/docs/Getting Started/On Completion.md
index cf20b5a3..575ae822 100644
--- a/docs/Getting Started/On Completion.md
+++ b/docs/Getting Started/On Completion.md
@@ -24,10 +24,12 @@ This feature is enabled by adding (*after* the description within a task) a fiel
 
 ## Supported actions
 
-At present, two "On Completion" ***actions*** are supported:
+At present, these "On Completion" ***actions*** are supported:
 
-1. **Ignore** &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Nothing will be done with the just-completed task.  (This is the default action.)
-2. **Delete** &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Removes the completed instance of the task.
+| Action     | Behaviour                                                                         |
+| ---------- | --------------------------------------------------------------------------------- |
+| **Keep**   | Nothing will be done with the just-completed task.  (This is the default action.) |
+| **Delete** | Removes the completed instance of the task.                                       |
 
 > [!tip]
 > Two additional ***actions*** are being considered for future implementation, namely moving a just-completed task to:
@@ -47,7 +49,7 @@ At present, two "On Completion" ***actions*** are supported:
 > ```text
 > # My Project Tasks
 > - [ ] Leave me alone
-> - [ ] Leave me alone too! 🏁 ignore
+> - [ ] Leave me alone too! 🏁 keep
 > - [ ] Delete me upon completion 🏁 delete
 > - [ ] Delete my completed instance, leave my next instance 📅 2021-05-20 🔁 every day when done 🏁 delete
 > ```
@@ -57,21 +59,15 @@ At present, two "On Completion" ***actions*** are supported:
 > ```text
 > # My Project Tasks
 > - [x] Leave me alone
-> - [x] Leave me alone too! 🏁 ignore
+> - [x] Leave me alone too! 🏁 keep
 > - [ ] Delete my completed instance, leave my next instance 📅 2021-05-21 🔁 every day when done 🏁 delete
 > ```
 
 > [!note] Note that
 >
-> - The task assigned the `ignore` action is treated the same as one that has no onCompletion field at all, and
+> - The task assigned the `keep` action is treated the same as one that has no onCompletion field at all, and
 > - The next instance of the recurring task has replaced the original, completed instance.
 
-> [!warning]
-> Currently, `🏁 ignore`  has some slightly confusing behaviour:
->
-> - When viewed in Reading mode, `🏁 ignore` is not shown.
-> - When tasks are completed, any `🏁 ignore` text is deleted.
-
 ## Assigning and changing a given task's "On Completion" action
 
 At present, the "On Completion" signifier and desired **Action** identifier have to be added to a task manually.
diff --git a/docs/Reference/Task Formats/Dataview Format.md b/docs/Reference/Task Formats/Dataview Format.md
index 5374641d..031a5d20 100644
--- a/docs/Reference/Task Formats/Dataview Format.md
+++ b/docs/Reference/Task Formats/Dataview Format.md
@@ -135,6 +135,8 @@ For more information, see [[Recurring Tasks]].
 
 <!-- snippet: DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-snippet.approved.md -->
 ```md
+- [ ] #task Keep this task when done
+- [ ] #task Keep this task when done too  [onCompletion:: keep]
 - [ ] #task Remove this task when done  [onCompletion:: delete]
 - [ ] #task Remove completed instance of this recurring task when done  [repeat:: every day]  [onCompletion:: delete]
 ```
diff --git a/docs/Reference/Task Formats/Tasks Emoji Format.md b/docs/Reference/Task Formats/Tasks Emoji Format.md
index 7ac20426..a938c26e 100644
--- a/docs/Reference/Task Formats/Tasks Emoji Format.md
+++ b/docs/Reference/Task Formats/Tasks Emoji Format.md
@@ -52,6 +52,8 @@ For more information, see [[Recurring Tasks]].
 
 <!-- snippet: DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-snippet.approved.md -->
 ```md
+- [ ] #task Keep this task when done
+- [ ] #task Keep this task when done too 🏁 keep
 - [ ] #task Remove this task when done 🏁 delete
 - [ ] #task Remove completed instance of this recurring task when done 🔁 every day 🏁 delete
 ```
diff --git a/src/Task/OnCompletion.ts b/src/Task/OnCompletion.ts
index e83188e1..a2c1e92c 100644
--- a/src/Task/OnCompletion.ts
+++ b/src/Task/OnCompletion.ts
@@ -3,6 +3,7 @@ import type { Task } from './Task';
 
 export enum OnCompletion {
     Ignore = '',
+    Keep = 'keep', // Like Ignore, but is visible on task lines
     Delete = 'delete',
 }
 
@@ -10,6 +11,8 @@ export function parseOnCompletionValue(inputOnCompletionValue: string) {
     const onCompletionString = inputOnCompletionValue.trim().toLowerCase();
     if (onCompletionString === 'delete') {
         return OnCompletion.Delete;
+    } else if (onCompletionString === 'keep') {
+        return OnCompletion.Keep;
     } else {
         return OnCompletion.Ignore;
     }
@@ -31,7 +34,11 @@ function keepTasks(originalTask: Task, changedStatusTask: Task) {
 
 export function handleOnCompletion(originalTask: Task, newTasks: Task[]): Task[] {
     const tasksArrayLength = newTasks.length;
-    if (originalTask.onCompletion === OnCompletion.Ignore || tasksArrayLength === 0) {
+    if (
+        originalTask.onCompletion === OnCompletion.Ignore ||
+        originalTask.onCompletion === OnCompletion.Keep ||
+        tasksArrayLength === 0
+    ) {
         return newTasks;
     }
     const changedStatusTask = newTasks[tasksArrayLength - 1];


Test patch:
diff --git a/tests/Task/OnCompletion.test.ts b/tests/Task/OnCompletion.test.ts
index 7e608033..e78046ff 100644
--- a/tests/Task/OnCompletion.test.ts
+++ b/tests/Task/OnCompletion.test.ts
@@ -40,6 +40,11 @@ describe('OnCompletion - parsing', () => {
         checkParseOnCompletionValue(input, OnCompletion.Delete);
     });
 
+    const keeps = ['keep', 'KEEP', ' keep '];
+    it.each(keeps)('should parse "%s" as OnCompletion.Keep', (input: string) => {
+        checkParseOnCompletionValue(input, OnCompletion.Keep);
+    });
+
     const ignores = ['', 'unknown'];
     it.each(ignores)('should parse "%s" as OnCompletion.Ignore', (input: string) => {
         checkParseOnCompletionValue(input, OnCompletion.Ignore);
@@ -125,6 +130,19 @@ describe('OnCompletion - cases where all tasks are retained', () => {
     });
 });
 
+describe('OnCompletion - "keep" action', () => {
+    it('should retain a task with "keep" Action upon completion', () => {
+        // Arrange
+        const task = makeTask('- [ ] A non-recurring task with "keep" Action 🏁 keep');
+
+        // Act
+        const tasks = applyStatusAndOnCompletionAction(task, Status.makeDone());
+
+        // Assert
+        expect(tasks.length).toEqual(1);
+    });
+});
+
 describe('OnCompletion - "delete" action', () => {
     it('should retain only the next instance of a recurring task with "delete" Action upon completion', () => {
         // Arrange
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-include.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-include.approved.md
index cf9f28e9..1c749b01 100644
--- a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-include.approved.md
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-include.approved.md
@@ -1,5 +1,7 @@
 <!-- placeholder to force blank line before included text -->
 
+- [ ] #task Keep this task when done
+- [ ] #task Keep this task when done too  [onCompletion:: keep]
 - [ ] #task Remove this task when done  [onCompletion:: delete]
 - [ ] #task Remove completed instance of this recurring task when done  [repeat:: every day]  [onCompletion:: delete]
 
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-snippet.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-snippet.approved.md
index d3332c42..25f85c48 100644
--- a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-snippet.approved.md
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_dataview-snippet.approved.md
@@ -1,2 +1,4 @@
+- [ ] #task Keep this task when done
+- [ ] #task Keep this task when done too  [onCompletion:: keep]
 - [ ] #task Remove this task when done  [onCompletion:: delete]
 - [ ] #task Remove completed instance of this recurring task when done  [repeat:: every day]  [onCompletion:: delete]
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-include.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-include.approved.md
index 341cd134..370a14a6 100644
--- a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-include.approved.md
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-include.approved.md
@@ -1,5 +1,7 @@
 <!-- placeholder to force blank line before included text -->
 
+- [ ] #task Keep this task when done
+- [ ] #task Keep this task when done too 🏁 keep
 - [ ] #task Remove this task when done 🏁 delete
 - [ ] #task Remove completed instance of this recurring task when done 🔁 every day 🏁 delete
 
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-snippet.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-snippet.approved.md
index 64e77907..ed9756f7 100644
--- a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-snippet.approved.md
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_OnCompletion_tasksPluginEmoji-snippet.approved.md
@@ -1,2 +1,4 @@
+- [ ] #task Keep this task when done
+- [ ] #task Keep this task when done too 🏁 keep
 - [ ] #task Remove this task when done 🏁 delete
 - [ ] #task Remove completed instance of this recurring task when done 🔁 every day 🏁 delete
diff --git a/tests/TestingTools/SampleTasks.ts b/tests/TestingTools/SampleTasks.ts
index a36fa8b2..9ff95b39 100644
--- a/tests/TestingTools/SampleTasks.ts
+++ b/tests/TestingTools/SampleTasks.ts
@@ -290,15 +290,14 @@ export class SampleTasks {
                 dueDate: null,
             }),
         });
-        const task1 = new TaskBuilder()
-            .description('#task Remove this task when done')
-            .onCompletion(OnCompletion.Delete)
-            .build();
-        const task2 = new TaskBuilder()
-            .description('#task Remove completed instance of this recurring task when done')
-            .onCompletion(OnCompletion.Delete)
-            .recurrence(everyDay)
-            .build();
-        return [task1, task2];
+        return [
+            new TaskBuilder().description('#task Keep this task when done').onCompletion(OnCompletion.Ignore),
+            new TaskBuilder().description('#task Keep this task when done too').onCompletion(OnCompletion.Keep),
+            new TaskBuilder().description('#task Remove this task when done').onCompletion(OnCompletion.Delete),
+            new TaskBuilder()
+                .description('#task Remove completed instance of this recurring task when done')
+                .onCompletion(OnCompletion.Delete)
+                .recurrence(everyDay),
+        ].map((builder) => builder.build());
     }
 }