Repository: obsidian-tasks-group/obsidian-tasks
Pull request #869 (2022-07-07 16:21:38)

fix: Stop 'Add & edit Task' breaking sub-tags of global filter
<!--- Provide a general summary of your changes in the Title above -->

## Description

For a task in the format of:
`- [ ] #global_filter <some description> #something_that_includes_global_filter ...`
i.e., when the task includes a tag that includes the global filter, when editing the task via the Edit Task dialog and updating any field, the task is rewritten to its origin note without the original global filter. In the example above, merely editing the task line via Edit Task and updating it, will rewrite the task as `<some description> #something_that_includes_global_filter ...`.
Worst part is if there are multiple tags that match the global filter, the Edit dialog actually corrupts them by removing the global filter match from one tag every time the task is updated...

This provides a simple fix.

## 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. -->
I'm using `#t` as a global filter and `#t/...` for various task tags, e.g. a typical task I have is `#t Some description #t/next #t/atOffice`.  
My tasks kept losing the global filter and their tags it took me a while to figure out why...

First time edit a task like the above it became `Some description #t/next #t/atOffice`, which could be fine, but on the next edit it became `Some description /next #t/atOffice` which was breaking my queries...

## 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. -->
Only on my own copy.
To my best understanding, this dialog has no automated tests.

## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Refactor (non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## 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`.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] My change has adequate Unit Test coverage.

By creating a Pull Request you agree to our [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md). For further guidance on contributing please see [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)


esm7: Sorry about that, will rebase the main branch instead of merging it next time.

claremacrae: > Sorry about that, will rebase the main branch instead of merging it next time.

Perfect - thanks very much.
claremacrae: Have you tested this in a vault without a global filter? What happens?

I'm fairly uncomfortable merging this without any tests for the logic in the modal/dialog. I think it's time to start adding them, first with a failing test for this capability...

A first step for that would be a PR to purely refactor the dialog code, to extract the guts of `onMount()` and `_onSubmit()` to stateless typescript code, so that the .svelte only deals with the UI widgets and has no - or minimal - logic.

Then there can be a second PR that fixes the behaviour.

I'm happy to pair on the adding of the first test, if it would help.
esm7: This refactoring sounds very doable, I might be able to do that next week.
But note that this is a data corruption bug - the plugin removes text without user intent, and as such it might be important to push a minimal patch first. Even if it goes through some more manual tests.

Also, the bug you linked to (https://github.com/obsidian-tasks-group/obsidian-tasks/issues/827) is interesting in this regard because it's the result of the same logic (removing the global filter and adding it back), but my change doesn't fix it. My patch can be extended to treat these cases (e.g. not remove the global filter if it's not a complete word, and then by the logic that I added, it will also not be "added back").
claremacrae: I did some basic testing. I haven't tested all paths through, but here is the first test I did:

With no global filter, on a blank line, open the Add/edit dialog, type wibble, press enter and the result is:

Before this PR:
`- [ ] wibble` (i.e. 1 space before wibble)
After this PR:
`- [ ]  wibble` (i.e. 2 spaces before wibble)

I suspect that this is because the logic to detect whether the global filter is present in the task looks like it doesn't allow for the global filter being blank, which is always present in the task, effectively...
esm7: Yes, caught that too following your previous comment. Embarrassing :facepalm: 
But it's easy to get this patch to be simple to verify!
claremacrae: So I think the paths to manually test are something like:

```
for each [with Tasks version prior to PR, with build of this PR]
    for each [with global filter, without global filter]
        for each [on blank line, on line with `- [ ] `, on existing task]
            open add/edit modal
            make edit
            save results
            check results
and compare the results of each step with the 2 different versions of the Plugin

esm7: > I volunteer to smoke test this on obsidian stable, if needed, following the protocol from above. Just let me know! (And then wait for timezone delay... :)

That's awesome, thank you!
I believe I'll fix the PR tomorrow and ping you when ready for testing :sunglasses: 
claremacrae: > That's awesome, thank you! I believe I'll fix the PR tomorrow and ping you when ready for testing 😎

Hi @esm7 - no pressure, but just to let you know I'll be doing a release soon - hopefully in the next day or so - so if you can see an easy way to fix the problem for users with no global tag filter, I'd be happy to include it in the release...


esm7: Thanks for the heads up! My week was busier than I anticipated so I'll only be able to do this on Sunday. I assume I'm going to miss the release window, but it's going to be a small fix, so I hope you'll feel confident to include it as part of some smaller patches release in the near future.
claremacrae: > Thanks for the heads up! My week was busier than I anticipated so I'll only be able to do this on Sunday. I assume I'm going to miss the release window, but it's going to be a small fix, so I hope you'll feel confident to include it as part of some smaller patches release in the near future.

Thank you, yes, that's absolutely fine. It can either be released on its own as a bug-fix. Or there are a few smaller new features coming soon too, so it could be included with then. Very happy either way...
esm7: I pushed a fix, and as part of @AnnaKornfeldSimpson 's offer to comprehensively test this, I felt more confident to rewrite this fix in a more comprehensive manner that also fixes https://github.com/obsidian-tasks-group/obsidian-tasks/issues/827.
**The new logic removes the global filter from the description only if it is a whole word, and returns it to the task when the dialog is closed only if it was indeed removed.**
This means that if the global filter is `#task` and the description is `some #task/group`, it will be shown in the dialog unmodified.
Only if the description includes `#task` as a whole word, e.g. `some #task`, it will be removed for the purpose of editing in the dialog, and be put back at the beginning (like the original logic) on save.
claremacrae: > I pushed a fix, and as part of @AnnaKornfeldSimpson 's offer to comprehensively test this, I felt more confident to rewrite this fix in a more comprehensive manner that also fixes #827.

Thanks for your work on this.

I feel in a difficult position now. This has gone from something like a 2-line fix that I could reason about (and was able to immediately spot a potential bug with, for when there was no global filter) - to a much more complicated thing, with multiple regular expressions.

The smoke tests are literally that - a very simple few-item run through to detect major errors. They do not replace developer testing, and would not have spotted the bug mentioned above.

Manual testing of this change would need a lot of care and thought about the happy paths, and potentially breaking changes.

And this would need to be repeated when future changes are mode to this code.

> **The new logic removes the global filter from the description only if it is a whole word, and returns it to the task when the dialog is closed only if it was indeed removed.**
> This means that if the global filter is `#task` and the description is `some #task/group`, it will be shown in the dialog unmodified.
> Only if the description includes `#task` as a whole word, e.g. `some #task`, it will be removed for the purpose of editing in the dialog, and be put back at the beginning (like the original logic) on save.

I am sorry, but with the larger scope and complexity of the code in this change now, my preference is to ask for:

1. the pure refactoring that we talked about earlier to be made as one PR - that adds some tests for the existing behaviour (it's fine if those tests record the current broken behaviour, for later updating once the behaviour is improved.)
2. then once that is merged, for a separate PR to be made that actually changes the behaviour, together with tests for the changed behaviour.

esm7: No worries, I will go back to a simple fix of the original 2-line fix; I may have gone too far compared to what can actually be tested thoroughly with the current infrastructure!
claremacrae: > No worries, I will go back to a simple fix of the original 2-line fix; I may have gone too far compared to what can actually be tested thoroughly with the current infrastructure!

That's great. Thank you for your understanding.
esm7: @claremacrae I have an idea!
What if I move this specific piece of logic, of removing the global filter and adding it back, to the `Task` class, and then cover it with tests using the existing infrastructure? Will you then be comfortable with a more comprehensive change?
claremacrae: Hi @esm7 

> @claremacrae I have an idea!
> What if I move this specific piece of logic, of removing the global filter and adding it back, to the `Task` class, and then cover it with tests using the existing infrastructure? Will you then be comfortable with a more comprehensive change?

How many PRs are you envisaging? 1 or 2?
claremacrae: > How many PRs are you envisaging? 1 or 2?

But to answer your question, yes, definitely happy with moving the logic out to somewhere where it can be tested... Basically, the less logic in UI code the better...

If you feel Tasks is the right (or geedo-enough) place, that's fine too.


claremacrae: > Whatever you prefer; adapting the current very thin logic (a few lines of simple code) to move to `Task` and adding tests to it is almost trivial.
> But doing the fix in the same PR is more convenient, that's for sure.

I understand that, from the author's perspective.

I'm thinking about 2 things:

## Long-term usefulness of the diffs history in the repo

This repo is set up to squash all PRs, so even if the combined single PR consisted of a refactoring commit and then a fixing commit, they would get squashed in to one.

And so future maintainers reviewing the history, e.g. via `git bisect` would never be able to separate out the refactoring change from the fixing change.

## Ease of reviewing the PR.

With a refactoring PR, the reviewer only has to convince themselves that the behaviour really hasn't changed.
With a bug-fix PR, they only have to understand the diff in fact does give the required change in behaviour.

If the PR does both, those two things become entangled, and it takes a lot longer to review.

I realise there will be a bit of a wait for the author between the two PRs, but in general, I will be able to merge two separate such PRs much more quickly than a single combined one.
claremacrae: Hallo @esm7, Is this one still needed, please, or is the next step a new PR off of main? Thanks.
esm7: Very needed indeed! Although it's been quite a journey since my tiny fix a few weeks ago, the whole discussion is here in this PR, so I was planning to finalize the fix based on the new refactored `main` in this PR.
Expecting to get to it today.
esm7: Done: updated this PR according to the new master.
Please note that by now, due to this PR going through many life stages, the commit history of the branch is very hard to follow.
But I thought that:
1. I should still merge from main rather than rebase, because rewriting the history of an active PR would make a future attempt to follow what we discussed here very difficult,
2. We rather use the same PR, and stick to the final result (via the 'Files Changed' diff), because it includes important discussions that also relate to other bugs and PRs.

Note that although the "base logic" is now covered with thorough tests, the actual flow of the UI is not, so the proposition of @AnnaKornfeldSimpson to run some well-defined manual tests is still very applicable.
claremacrae: Thank you!

Patch:
diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md
index c45c958b..ba71e54a 100644
--- a/.github/ISSUE_TEMPLATE/issue-template.md
+++ b/.github/ISSUE_TEMPLATE/issue-template.md
@@ -25,6 +25,7 @@ If you want to report a bug, please follow the guide lines below to help me reso
 <!-- Which exact steps can I take to reproduce the issue? -->
 
 ## Context (Environment)
+
 * Obsidian version:
 * Tasks version:
 * [ ] I have tried it with all other plugins disabled and the error still occurs
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 0a92cdf9..5e22dc4b 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,6 +1,6 @@
 <!--- Provide a general summary of your changes in the Title above -->
 
-## Description
+# Description
 <!--- Describe your changes in detail -->
 
 ## Motivation and Context
@@ -12,7 +12,7 @@
 <!--- Include details of your testing environment, tests ran to see how -->
 <!--- your change affects other areas of the code, etc. -->
 
-## Screenshots (if appropriate):
+## Screenshots (if appropriate)
 
 ## Types of changes
 <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
@@ -21,7 +21,7 @@
 - [ ] New feature (non-breaking change which adds functionality)
 - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
 
-## Checklist:
+## 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! -->
 - [ ] My code follows the code style of this project and passes `yarn run lint`.
diff --git a/README.md b/README.md
index 55caff95..d4578492 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ For changes in each release, please check the releases page: <https://github.com
 
 ## Screenshots
 
-- _All screenshots assume the [global filter](#filtering-checklist-items) `#task` which is not set by default (see also [installation](#installation))._
+- _All screenshots assume the [global filter](https://obsidian-tasks-group.github.io/obsidian-tasks/getting-started/global-filter/) `#task` which is not set by default (see also [installation](https://obsidian-tasks-group.github.io/obsidian-tasks/installation/))._
 - _The theme is default Obsidian theme._
 
 ![ACME Tasks](https://github.com/obsidian-tasks-group/obsidian-tasks/raw/gh-pages/resources/screenshots/acme.png)
diff --git a/docs/getting-started/recurring-tasks.md b/docs/getting-started/recurring-tasks.md
index c728833d..086f6bdb 100644
--- a/docs/getting-started/recurring-tasks.md
+++ b/docs/getting-started/recurring-tasks.md
@@ -135,7 +135,9 @@ Examples of possible recurrence rules (mix and match as desired; these should be
 - `🔁 every 3 weeks on Friday`
 - `🔁 every 2 months`
 - `🔁 every month on the 1st`
+- `🔁 every month on the last`
 - `🔁 every 6 months on the 2nd Wednesday`
 - `🔁 every January on the 15th`
+- `🔁 every February on the last`
 - `🔁 every April and December on the 1st and 24th` (meaning every _April 1st_ and _December 24th_)
 - `🔁 every year`
diff --git a/manifest.json b/manifest.json
index e3e4fee0..7e824ec8 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
 {
   "id": "obsidian-tasks-plugin",
   "name": "Tasks",
-  "version": "1.10.0",
+  "version": "1.11.0",
   "minAppVersion": "0.14.6",
   "description": "Task management for Obsidian",
   "author": "Martin Schenck and Clare Macrae",
diff --git a/package.json b/package.json
index 0e681145..6722c0e0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "obsidian-tasks",
-  "version": "1.10.0",
+  "version": "1.11.0",
   "description": "Task management for the Obsidian knowledge base",
   "main": "main.js",
   "scripts": {
@@ -27,8 +27,8 @@
     "@codemirror/view": "^6.0.2",
     "@tsconfig/svelte": "^3.0.0",
     "@types/jest": "^28.1.6",
-    "@typescript-eslint/eslint-plugin": "^5.30.7",
-    "@typescript-eslint/parser": "^5.30.7",
+    "@typescript-eslint/eslint-plugin": "^5.31.0",
+    "@typescript-eslint/parser": "^5.31.0",
     "async-mutex": "^0.3.1",
     "builtin-modules": "^3.3.0",
     "esbuild": "0.14.49",
@@ -39,7 +39,7 @@
     "eslint-plugin-prettier": "^4.2.1",
     "jest": "^28.1.3",
     "jest-environment-jsdom": "^28.1.3",
-    "markdownlint-cli2": "^0.4.0",
+    "markdownlint-cli2": "^0.5.0",
     "moment": "^2.29.4",
     "obsidian": "0.14.6",
     "prettier": "^2.2.1",
diff --git a/src/Task.ts b/src/Task.ts
index 873831ed..4365d24b 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -862,11 +862,35 @@ export class Task {
         )})`;
     }
 
+    /**
+     * Escape a string so it can be used as part of a RegExp literally.
+     * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
+     */
+    private escapeRegExp(s: string) {
+        return s.replace(/([.*+?^=!:${}()|[]\/\\])/g, '\\$1');
+    }
+
+    /**
+     * Search for the global filter for the purpose of removing it from the description, but do so only
+     * if it is a separate word (preceding the beginning of line or a space and followed by the end of line
+     * or a space), because we don't want to cut-off nested tags like #task/subtag.
+     * If the global filter exists as part of a nested tag, we keep it untouched.
+     */
     public getDescriptionWithoutGlobalFilter() {
         const { globalFilter } = getSettings();
-        return this.description
-            .replace(globalFilter, '')
-            .replace('  ', ' ')
-            .trim();
+        let description = this.description;
+        if (globalFilter.length === 0) return description;
+        // This matches the global filter (after escaping it) only when it's a complete word
+        const globalFilterRegex = RegExp(
+            '(^|\\s)' + this.escapeRegExp(globalFilter) + '($|\\s)',
+            'ug',
+        );
+        if (this.description.search(globalFilterRegex) > -1) {
+            description = description
+                .replace(globalFilterRegex, '$1$2')
+                .replace('  ', ' ')
+                .trim();
+        }
+        return description;
     }
 }
diff --git a/src/ui/EditTask.svelte b/src/ui/EditTask.svelte
index 5356fdd7..2c26f4d7 100644
--- a/src/ui/EditTask.svelte
+++ b/src/ui/EditTask.svelte
@@ -42,6 +42,7 @@
     let parsedDueDate: string = '';
     let parsedRecurrence: string = '';
     let parsedDone: string = '';
+    let addGlobalFilterOnSave: boolean = false;
 
     // 'weekend' abbreviation ommitted due to lack of space.
     let datePlaceholder =
@@ -106,8 +107,17 @@
         parsedDone = parseDate('done', editableTask.doneDate);
     }
 
+
     onMount(() => {
+        const { globalFilter } = getSettings();
         const description = task.getDescriptionWithoutGlobalFilter();
+        // If we're displaying to the user the description without the global filter (i.e. it was removed in the method
+        // above), or if the description did not include a global filter in the first place, we'll add the global filter
+        // when saving the task.
+        // Another special case is when the global filter is empty: in this case there's an "empty" match in the `indexOf`
+        // (it returns 0), and thus we *don't* set addGlobalFilterOnSave.
+        if (description != task.description || description.indexOf(globalFilter) == -1)
+            addGlobalFilterOnSave = true;
         let priority: 'none' | 'low' | 'medium' | 'high' = 'none';
         if (task.priority === Priority.Low) {
             priority = 'low';
@@ -139,7 +149,7 @@
     const _onSubmit = () => {
         const { globalFilter } = getSettings();
         let description = editableTask.description.trim();
-        if (!description.includes(globalFilter)) {
+        if (addGlobalFilterOnSave) {
             description = globalFilter + ' ' + description;
         }
 
diff --git a/versions.json b/versions.json
index d362dc41..4fb059b5 100644
--- a/versions.json
+++ b/versions.json
@@ -34,5 +34,6 @@
   "1.8.1": "0.13.21",
   "1.8.2": "0.13.21",
   "1.9.0": "0.14.6",
-  "1.10.0": "0.14.6"
+  "1.10.0": "0.14.6",
+  "1.11.0": "0.14.6"
 }
diff --git a/yarn.lock b/yarn.lock
index 8a3fee2c..b79321af 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1014,14 +1014,14 @@
   dependencies:
     "@types/yargs-parser" "*"
 
-"@typescript-eslint/eslint-plugin@^5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.7.tgz#1621dabc1ae4084310e19e9efc80dfdbb97e7493"
-  integrity sha512-l4L6Do+tfeM2OK0GJsU7TUcM/1oN/N25xHm3Jb4z3OiDU4Lj8dIuxX9LpVMS9riSXQs42D1ieX7b85/r16H9Fw==
-  dependencies:
-    "@typescript-eslint/scope-manager" "5.30.7"
-    "@typescript-eslint/type-utils" "5.30.7"
-    "@typescript-eslint/utils" "5.30.7"
+"@typescript-eslint/eslint-plugin@^5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz#cae1967b1e569e6171bbc6bec2afa4e0c8efccfe"
+  integrity sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==
+  dependencies:
+    "@typescript-eslint/scope-manager" "5.31.0"
+    "@typescript-eslint/type-utils" "5.31.0"
+    "@typescript-eslint/utils" "5.31.0"
     debug "^4.3.4"
     functional-red-black-tree "^1.0.1"
     ignore "^5.2.0"
@@ -1029,69 +1029,69 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/parser@^5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.7.tgz#99d09729392aec9e64b1de45cd63cb81a4ddd980"
-  integrity sha512-Rg5xwznHWWSy7v2o0cdho6n+xLhK2gntImp0rJroVVFkcYFYQ8C8UJTSuTw/3CnExBmPjycjmUJkxVmjXsld6A==
+"@typescript-eslint/parser@^5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.31.0.tgz#7f42d7dcc68a0a6d80a0f3d9a65063aee7bb8d2c"
+  integrity sha512-UStjQiZ9OFTFReTrN+iGrC6O/ko9LVDhreEK5S3edmXgR396JGq7CoX2TWIptqt/ESzU2iRKXAHfSF2WJFcWHw==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.30.7"
-    "@typescript-eslint/types" "5.30.7"
-    "@typescript-eslint/typescript-estree" "5.30.7"
+    "@typescript-eslint/scope-manager" "5.31.0"
+    "@typescript-eslint/types" "5.31.0"
+    "@typescript-eslint/typescript-estree" "5.31.0"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.7.tgz#8269a931ef1e5ae68b5eb80743cc515c4ffe3dd7"
-  integrity sha512-7BM1bwvdF1UUvt+b9smhqdc/eniOnCKxQT/kj3oXtj3LqnTWCAM0qHRHfyzCzhEfWX0zrW7KqXXeE4DlchZBKw==
+"@typescript-eslint/scope-manager@5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz#f47a794ba84d9b818ab7f8f44fff55a61016c606"
+  integrity sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==
   dependencies:
-    "@typescript-eslint/types" "5.30.7"
-    "@typescript-eslint/visitor-keys" "5.30.7"
+    "@typescript-eslint/types" "5.31.0"
+    "@typescript-eslint/visitor-keys" "5.31.0"
 
-"@typescript-eslint/type-utils@5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.30.7.tgz#5693dc3db6f313f302764282d614cfdbc8a9fcfd"
-  integrity sha512-nD5qAE2aJX/YLyKMvOU5jvJyku4QN5XBVsoTynFrjQZaDgDV6i7QHFiYCx10wvn7hFvfuqIRNBtsgaLe0DbWhw==
+"@typescript-eslint/type-utils@5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz#70a0b7201360b5adbddb0c36080495aa08f6f3d9"
+  integrity sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==
   dependencies:
-    "@typescript-eslint/utils" "5.30.7"
+    "@typescript-eslint/utils" "5.31.0"
     debug "^4.3.4"
     tsutils "^3.21.0"
 
-"@typescript-eslint/types@5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.7.tgz#18331487cc92d0f1fb1a6f580c8ec832528079d0"
-  integrity sha512-ocVkETUs82+U+HowkovV6uxf1AnVRKCmDRNUBUUo46/5SQv1owC/EBFkiu4MOHeZqhKz2ktZ3kvJJ1uFqQ8QPg==
+"@typescript-eslint/types@5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.31.0.tgz#7aa389122b64b18e473c1672fb3b8310e5f07a9a"
+  integrity sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==
 
-"@typescript-eslint/typescript-estree@5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.7.tgz#05da9f1b281985bfedcf62349847f8d168eecc07"
-  integrity sha512-tNslqXI1ZdmXXrHER83TJ8OTYl4epUzJC0aj2i4DMDT4iU+UqLT3EJeGQvJ17BMbm31x5scSwo3hPM0nqQ1AEA==
+"@typescript-eslint/typescript-estree@5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz#eb92970c9d6e3946690d50c346fb9b1d745ee882"
+  integrity sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==
   dependencies:
-    "@typescript-eslint/types" "5.30.7"
-    "@typescript-eslint/visitor-keys" "5.30.7"
+    "@typescript-eslint/types" "5.31.0"
+    "@typescript-eslint/visitor-keys" "5.31.0"
     debug "^4.3.4"
     globby "^11.1.0"
     is-glob "^4.0.3"
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/utils@5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.30.7.tgz#7135be070349e9f7caa262b0ca59dc96123351bb"
-  integrity sha512-Z3pHdbFw+ftZiGUnm1GZhkJgVqsDL5CYW2yj+TB2mfXDFOMqtbzQi2dNJIyPqPbx9mv2kUxS1gU+r2gKlKi1rQ==
+"@typescript-eslint/utils@5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.31.0.tgz#e146fa00dca948bfe547d665b2138a2dc1b79acd"
+  integrity sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==
   dependencies:
     "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.30.7"
-    "@typescript-eslint/types" "5.30.7"
-    "@typescript-eslint/typescript-estree" "5.30.7"
+    "@typescript-eslint/scope-manager" "5.31.0"
+    "@typescript-eslint/types" "5.31.0"
+    "@typescript-eslint/typescript-estree" "5.31.0"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
-"@typescript-eslint/visitor-keys@5.30.7":
-  version "5.30.7"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.7.tgz#c093abae75b4fd822bfbad9fc337f38a7a14909a"
-  integrity sha512-KrRXf8nnjvcpxDFOKej4xkD7657+PClJs5cJVSG7NNoCNnjEdc46juNAQt7AyuWctuCgs6mVRc1xGctEqrjxWw==
+"@typescript-eslint/visitor-keys@5.31.0":
+  version "5.31.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz#b0eca264df01ce85dceb76aebff3784629258f54"
+  integrity sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==
   dependencies:
-    "@typescript-eslint/types" "5.30.7"
+    "@typescript-eslint/types" "5.31.0"
     eslint-visitor-keys "^3.3.0"
 
 abab@^2.0.5, abab@^2.0.6:
@@ -1211,11 +1211,6 @@ array-union@^2.1.0:
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
   integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
 
-array-union@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/array-union/-/array-union-3.0.1.tgz#da52630d327f8b88cfbfb57728e2af5cd9b6b975"
-  integrity sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==
-
 array.prototype.flat@^1.2.5:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b"
@@ -1321,7 +1316,7 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
+braces@^3.0.2, braces@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
   integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -1684,10 +1679,10 @@ emoji-regex@^8.0.0:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
-entities@~2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
-  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
+entities@~3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
+  integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
 
 error-ex@^1.3.1:
   version "1.3.2"
@@ -2114,7 +2109,7 @@ fast-diff@^1.1.2:
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
   integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
 
-fast-glob@^3.2.7, fast-glob@^3.2.9:
+fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9:
   version "3.2.11"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
   integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
@@ -2322,15 +2317,14 @@ globals@^13.15.0:
   dependencies:
     type-fest "^0.20.2"
 
-globby@12.1.0:
-  version "12.1.0"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-12.1.0.tgz#471757d6d9d25651b655b1da3eae1e25209f86a5"
-  integrity sha512-YULDaNwsoUZkRy9TWSY/M7Obh0abamTKoKzTfOI3uU+hfpX2FZqOq8LFDxsjYheF1RH7ITdArgbQnsNBFgcdBA==
+globby@13.1.2:
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515"
+  integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==
   dependencies:
-    array-union "^3.0.1"
     dir-glob "^3.0.1"
-    fast-glob "^3.2.7"
-    ignore "^5.1.9"
+    fast-glob "^3.2.11"
+    ignore "^5.2.0"
     merge2 "^1.4.1"
     slash "^4.0.0"
 
@@ -2433,7 +2427,7 @@ iconv-lite@0.6.3:
   dependencies:
     safer-buffer ">= 2.1.2 < 3.0.0"
 
-ignore@^5.1.9, ignore@^5.2.0:
+ignore@^5.2.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
   integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
@@ -3158,10 +3152,10 @@ lines-and-columns@^1.1.6:
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
   integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
 
-linkify-it@^3.0.1:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e"
-  integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==
+linkify-it@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
+  integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
   dependencies:
     uc.micro "^1.0.1"
 
@@ -3223,14 +3217,14 @@ makeerror@1.0.12:
   dependencies:
     tmpl "1.0.5"
 
-markdown-it@12.3.2:
-  version "12.3.2"
-  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90"
-  integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==
+markdown-it@13.0.1:
+  version "13.0.1"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430"
+  integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==
   dependencies:
     argparse "^2.0.1"
-    entities "~2.1.0"
-    linkify-it "^3.0.1"
+    entities "~3.0.1"
+    linkify-it "^4.0.1"
     mdurl "^1.0.1"
     uc.micro "^1.0.5"
 
@@ -3239,30 +3233,24 @@ markdownlint-cli2-formatter-default@0.0.3:
   resolved "https://registry.yarnpkg.com/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.3.tgz#5aecd6e576ad18801b76e58bbbaf0e916c583ab8"
   integrity sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw==
 
-markdownlint-cli2@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/markdownlint-cli2/-/markdownlint-cli2-0.4.0.tgz#9e29150278fd3f1fe31a05fc46bd95d2370e8758"
-  integrity sha512-EcwP5tAbyzzL3ACI0L16LqbNctmh8wNX56T+aVvIxWyTAkwbYNx2V7IheRkXS3mE7R/pnaApZ/RSXcXuzRVPjg==
+markdownlint-cli2@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/markdownlint-cli2/-/markdownlint-cli2-0.5.0.tgz#4d2c3545d2ab4e8b1d38dcb81e207602061eba32"
+  integrity sha512-lUirs0NEuIOIwfWCjVqBBKB5VXD+OceyviHuyjwOUAj/DWm5Pf8iJ23nMo/0ySH6svQJ9ddC+Dsq/X5Hd+yvnA==
   dependencies:
-    globby "12.1.0"
-    markdownlint "0.25.1"
+    globby "13.1.2"
+    markdownlint "0.26.1"
     markdownlint-cli2-formatter-default "0.0.3"
-    markdownlint-rule-helpers "0.16.0"
-    micromatch "4.0.4"
-    strip-json-comments "4.0.0"
-    yaml "1.10.2"
-
-markdownlint-rule-helpers@0.16.0:
-  version "0.16.0"
-  resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz#c327f72782bd2b9475127a240508231f0413a25e"
-  integrity sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w==
+    micromatch "4.0.5"
+    strip-json-comments "5.0.0"
+    yaml "2.1.1"
 
-markdownlint@0.25.1:
-  version "0.25.1"
-  resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.25.1.tgz#df04536607ebeeda5ccd5e4f38138823ed623788"
-  integrity sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==
+markdownlint@0.26.1:
+  version "0.26.1"
+  resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.26.1.tgz#0f6ea2d0fc08c4f41c6e001b467736405327a392"
+  integrity sha512-8sLz1ktz5s4E0IDum2H9aiWLQU7RA5Eket9HUW5IRwfFnW2RD2ZyqYePW+z71tMc7lrFZc1+yPmlN9lirbJnlg==
   dependencies:
-    markdown-it "12.3.2"
+    markdown-it "13.0.1"
 
 mdurl@^1.0.1:
   version "1.0.1"
@@ -3279,15 +3267,7 @@ merge2@^1.3.0, merge2@^1.4.1:
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-micromatch@4.0.4:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
-  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
-  dependencies:
-    braces "^3.0.1"
-    picomatch "^2.2.3"
-
-micromatch@^4.0.4:
+micromatch@4.0.5, micromatch@^4.0.4:
   version "4.0.5"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
   integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
@@ -3955,10 +3935,10 @@ strip-indent@^3.0.0:
   dependencies:
     min-indent "^1.0.0"
 
-strip-json-comments@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-4.0.0.tgz#6fd3a79f1b956905483769b0bf66598b8f87da50"
-  integrity sha512-LzWcbfMbAsEDTRmhjWIioe8GcDRl0fa35YMXFoJKDdiD/quGFmjJjdgPjFJJNwCMaLyQqFIDqCdHD2V4HfLgYA==
+strip-json-comments@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-5.0.0.tgz#ec101b766476a703031bc607e3c712569de2aa06"
+  integrity sha512-V1LGY4UUo0jgwC+ELQ2BNWfPa17TIuwBLg+j1AA/9RPzKINl1lhxVEu2r+ZTTO8aetIsUzE5Qj6LMSBkoGYKKw==
 
 strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   version "3.1.1"
@@ -4348,10 +4328,10 @@ yallist@^4.0.0:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
-yaml@1.10.2:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
-  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+yaml@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec"
+  integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==
 
 yargs-parser@^21.0.0, yargs-parser@^21.0.1:
   version "21.0.1"


Test patch:
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index 8a4412b0..7ea6a26c 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -394,9 +394,27 @@ describe('toggle done', () => {
         expect(toggled).not.toBeNull();
         expect(toggled!.status).toStrictEqual(Status.Done);
         expect(toggled!.doneDate).not.toBeNull();
+        expect(toggled!.originalStatusCharacter).toStrictEqual('x');
         expect(toggled!.blockLink).toEqual(' ^my-precious');
     });
 
+    it('removes done date after untoggle', () => {
+        // Arrange
+        const line = '- [x] I thought I finished ✅ 2021-09-12';
+
+        // Act
+        const task: Task = fromLine({
+            line,
+        }) as Task;
+        const toggled: Task = task.toggle()[0];
+
+        // Assert
+        expect(toggled).not.toBeNull();
+        expect(toggled!.status).toStrictEqual(Status.Todo);
+        expect(toggled!.originalStatusCharacter).toStrictEqual(' ');
+        expect(toggled!.doneDate).toBeNull();
+    });
+
     type RecurrenceCase = {
         interval: string;
         due?: string;
@@ -953,14 +971,13 @@ describe('check removal of the global filter', () => {
             globalFilter: '#task',
             markdownTask:
                 '- [ ] task with an extension of the global filter #task/with/extension',
-            // This is not the behavior we eventually want, but this is the existing situation
             expectedDescription:
-                'task with an extension of the global filter /with/extension',
+                'task with an extension of the global filter #task/with/extension',
         },
         {
             globalFilter: '#t',
             markdownTask: '- [ ] task with #t multiple global filters #t',
-            expectedDescription: 'task with multiple global filters #t',
+            expectedDescription: 'task with multiple global filters',
         },
     ])(
         'should parse "$markdownTask" and extract "$expectedDescription"',