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

Issue:
'remove global filter' option breaks display of sub-tags of global filter
### Please check that this issue hasn't been reported before.

- [X] I searched previous [Bug Reports](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22+sort%3Acomments-desc) didn't find any similar reports.

### Expected Behavior

Global Filter removal in queries shall be done as a word, not as a substring.

For example if 
- GF = `#task`
- Global Filter removal option is enabled
- task = `- [ ] #task/something else`

The task shall be rendered as `- [ ] #task/something else`.

The only change in the behaviour shall be regarding the query render - the Global Filter shall not be removed if it is a substring of a bigger word. Even if it not a tag.

### Current behaviour

Now such a task is rendered as `- [ ] /something else`

![Снимок экрана 2023-06-06 в 00 20 49](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/93825870/edad0473-020b-49f1-a656-07877f7e7b8e)


### Steps to reproduce

Source of the test file on the screenshot:
````
- [ ] #task/something else
```tasks
filename includes testfile
```
````

Set Global Filter to #task and enable the Global Filter removal option.

### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

1.3.5

### Tasks Plugin Version

3.10.0

### Checks

- [ ] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

I will try to make a PR =)

Pull request:
fix: 'remove global filter' broke display of sub-tags of global filter
<!--- Provide a general summary of your changes in the Title above -->

# Description

When Global Filter removal option is enabled, it was removed as a substring for query rendering.

This makes Global Filter being removed as a word.

## Motivation and Context

Fixes #2021 .

## How has this been tested?

Unit test: new test to show the fix added.
Smoke test on the Tasks demo vault (Screenshots below).

## Screenshots (if appropriate)

Before

![Снимок экрана 2023-06-06 в 00 20 49](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/93825870/205b9238-7fe6-449b-bd57-b7d19ee45168)

After

![Снимок экрана 2023-06-06 в 01 21 46](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/93825870/4fc8af45-3f99-492d-81ce-a54e329e47a8)


## 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] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [ ] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [ ] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [ ] **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))
- [ ] **Contributing Guidelines** (prefix: `contrib` - any improvements to documentation content **for contributors** - see [Contributing to Tasks](https://publish.obsidian.md/tasks-contributing/))

Internal changes:

- [ ] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## 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`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [ ] 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.
-->

- [ ] My contribution follow this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [ ] 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/src/Config/GlobalFilter.ts b/src/Config/GlobalFilter.ts
index 593a1786..9158235a 100644
--- a/src/Config/GlobalFilter.ts
+++ b/src/Config/GlobalFilter.ts
@@ -34,6 +34,15 @@ export class GlobalFilter {
         return GlobalFilter.get() + ' ' + description;
     }
 
+    static removeAsWordFromDependingOnSettings(description: string): string {
+        const { removeGlobalFilter } = getSettings();
+        if (removeGlobalFilter) {
+            return GlobalFilter.removeAsWordFrom(description);
+        }
+
+        return description;
+    }
+
     /**
      * 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
@@ -59,12 +68,4 @@ export class GlobalFilter {
         const globalFilter = GlobalFilter.get();
         return description.replace(globalFilter, '').trim();
     }
-
-    static removeAsSubstringFromDependingOnSettings(description: string): string {
-        const { removeGlobalFilter } = getSettings();
-        if (removeGlobalFilter) {
-            return GlobalFilter.removeAsSubstringFrom(description);
-        }
-        return description;
-    }
 }
diff --git a/src/TaskLineRenderer.ts b/src/TaskLineRenderer.ts
index 55b20865..561d2c3c 100644
--- a/src/TaskLineRenderer.ts
+++ b/src/TaskLineRenderer.ts
@@ -138,7 +138,7 @@ async function taskToHtml(
         let componentString = emojiSerializer.componentToString(task, taskLayout, component);
         if (componentString) {
             if (component === 'description') {
-                componentString = GlobalFilter.removeAsSubstringFromDependingOnSettings(componentString);
+                componentString = GlobalFilter.removeAsWordFromDependingOnSettings(componentString);
             }
             // Create the text span that will hold the rendered component
             const span = document.createElement('span');


Test patch:
diff --git a/tests/GlobalFilter.test.ts b/tests/GlobalFilter.test.ts
index 1a0432ea..5b8838dd 100644
--- a/tests/GlobalFilter.test.ts
+++ b/tests/GlobalFilter.test.ts
@@ -1,5 +1,5 @@
-import { GlobalFilter } from '../src/Config/GlobalFilter';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
+import { GlobalFilter } from '../src/Config/GlobalFilter';
 
 describe('Global Filter tests', () => {
     afterEach(() => {
@@ -105,7 +105,7 @@ describe('Global Filter tests with Remove Global Filter Setting', () => {
         updateSettings({ removeGlobalFilter: false });
 
         // Assert
-        expect(GlobalFilter.removeAsSubstringFromDependingOnSettings('This is absolutely todo')).toEqual(
+        expect(GlobalFilter.removeAsWordFromDependingOnSettings('This is absolutely todo')).toEqual(
             'This is absolutely todo',
         );
     });
@@ -116,10 +116,30 @@ describe('Global Filter tests with Remove Global Filter Setting', () => {
         updateSettings({ removeGlobalFilter: true });
 
         // Assert
-        expect(GlobalFilter.removeAsSubstringFromDependingOnSettings('This is absolutely todo')).toEqual(
+        expect(GlobalFilter.removeAsWordFromDependingOnSettings('This is absolutely todo')).toEqual(
             'This is absolutely',
         );
     });
+
+    it.each([
+        // Global Filter is a tag
+        ['#task', '#task/stage at the beginning'],
+        ['#task', 'in the #task/stage middle'],
+        ['#task', 'at the end #task/stage'],
+
+        // Global Filter is not a tag
+        ['TODO', 'TODO/maybe at the beginning'],
+        ['TODO', 'in the TODO/maybe middle'],
+        ['TODO', 'at the end TODO/maybe'],
+    ])(
+        'should not remove Global Filter (%s) if it is in a substring for a task "- [ ] %s"',
+        async (globalFilter: string, description: string) => {
+            updateSettings({ removeGlobalFilter: true });
+            GlobalFilter.set(globalFilter);
+
+            expect(GlobalFilter.removeAsWordFrom(description)).toEqual(description);
+        },
+    );
 });
 
 describe('check removal of the global filter', () => {
diff --git a/tests/TaskLineRenderer.test.ts b/tests/TaskLineRenderer.test.ts
index 891c58f7..4cf398f9 100644
--- a/tests/TaskLineRenderer.test.ts
+++ b/tests/TaskLineRenderer.test.ts
@@ -109,24 +109,32 @@ describe('task line rendering', () => {
         expect((internalDescriptionSpan as HTMLSpanElement).innerText).toEqual('This is a simple task');
     });
 
-    it('hides the global filter if and only if required', async () => {
-        const getDescriptionTest = async () => {
-            const taskLine = '- [ ] This is a simple task with a #global filter';
-            const task = fromLine({
-                line: taskLine,
-            });
-            const parentRender = await createMockParentAndRender(task);
-            return getDescriptionText(parentRender);
-        };
+    const getDescriptionTest = async (taskLine: string) => {
+        const task = fromLine({
+            line: taskLine,
+        });
+        const parentRender = await createMockParentAndRender(task);
+        return getDescriptionText(parentRender);
+    };
+
+    it('should render Global Filter when the Remove Global Filter is off', async () => {
+        updateSettings({ removeGlobalFilter: false });
+        GlobalFilter.set('#global');
+
+        const taskLine = '- [ ] This is a simple task with a #global filter';
+        const descriptionWithFilter = await getDescriptionTest(taskLine);
 
-        const descriptionWithFilter = await getDescriptionTest();
         expect(descriptionWithFilter).toEqual('This is a simple task with a #global filter');
+    });
 
+    it('should not render Global Filter when the Remove Global Filter is on', async () => {
         updateSettings({ removeGlobalFilter: true });
         GlobalFilter.set('#global');
-        const descriptionWithoutFilter = await getDescriptionTest();
-        expect(descriptionWithoutFilter).toEqual('This is a simple task with a  filter');
-        resetSettings();
+
+        const taskLine = '- [ ] #global/subtag-shall-stay This is a simple task with a #global filter';
+        const descriptionWithoutFilter = await getDescriptionTest(taskLine);
+
+        expect(descriptionWithoutFilter).toEqual('#global/subtag-shall-stay This is a simple task with a filter');
     });
 
     const testLayoutOptions = async (