Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1101 (2022-09-02 18:26:07)

docs: Fix "Toggle checkbox status" command name in README
The name of the Obsidian command is “Toggle checkbox status,” not “Toggle checklist status.”

<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

## 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. -->

## 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)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [ ] **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)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)
- [ ] **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:

- [ ] **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://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [ ] 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/README.md b/README.md
index d4578492..7a263206 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ Follow the steps below to install Tasks.
 1. Search for "Tasks" in Obsidian's community plugins browser
 2. Enable the plugin in your Obsidian settings (find "Tasks" under "Community plugins").
 3. Check the settings. It makes sense to set the global filter early on (if you want one).
-4. Replace the "Toggle checklist status" hotkey with "Tasks: Toggle Done".
+4. Replace the "Toggle checkbox status" hotkey with "Tasks: Toggle Done".
     - I recommend you remove the original toggle hotkey and set the "Tasks" toggle to `Ctrl + Enter` (or `Cmd + Enter` on a mac).
 
 ## Documentation
diff --git a/docs/queries/filters.md b/docs/queries/filters.md
index 35cf4931..9f07fe37 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -208,6 +208,8 @@ These filters allow searching for tasks in particular files and sections of file
 
 ### File Path
 
+Note that the path includes the `.md` extension.
+
 - `path (includes|does not include) <path>`
   - Matches case-insensitive (disregards capitalization).
 - `path (regex matches|regex does not match) /<JavaScript-style Regex>/`
@@ -216,6 +218,18 @@ These filters allow searching for tasks in particular files and sections of file
 
 > `regex matches` and `regex does not match` were introduced in Tasks 1.12.0.
 
+### File Name
+
+> Introduced in Tasks 1.13.0.
+
+Note that the file name includes the `.md` extension.
+
+- `filename (includes|does not include) <filename>`
+  - Matches case-insensitive (disregards capitalization).
+- `filename (regex matches|regex does not match) /<JavaScript-style Regex>/`
+  - Does regular expression match (case-sensitive by default).
+  - Essential reading: [Regular Expression Searches]({{ site.baseurl }}{% link queries/regular-expressions.md %}).
+
 ### Heading
 
 - `heading (includes|does not include) <string>`
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index 3b6d4f63..8f3f49d4 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -29,7 +29,7 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `path (includes, does not include) <path>`<br>`path (regex matches, regex does not match) /regex/i`                 | `sort by path`                              | `group by path`       |                        |
 |                                                                                                                     |                                             | `group by root`       |                        |
 |                                                                                                                     |                                             | `group by folder`     |                        |
-|                                                                                                                     |                                             | `group by filename`   |                        |
+| `filename (includes, does not include) <filename>`<br>`filename (regex matches, regex does not match) /regex/i`     |                                             | `group by filename`   |                        |
 | `heading (includes, does not include) <string>`<br>`heading (regex matches, regex does not match) /regex/i`         |                                             | `group by heading`    |                        |
 |                                                                                                                     |                                             | `group by backlink`   | `hide backlink`        |
 | `description (includes, does not include) <string>`<br>`description (regex matches, regex does not match) /regex/i` | `sort by description`                       |                       |                        |
diff --git a/src/Query/Filter/FilenameField.ts b/src/Query/Filter/FilenameField.ts
new file mode 100644
index 00000000..c6d30423
--- /dev/null
+++ b/src/Query/Filter/FilenameField.ts
@@ -0,0 +1,27 @@
+import type { Task } from '../../Task';
+import { TextField } from './TextField';
+
+/** Support the 'filename' search instruction.
+ *
+ * Note that the current implementation also searches the file extension,
+ * so 'filename includes .md' will typically match all tasks.
+ *
+ */
+export class FilenameField extends TextField {
+    public fieldName(): string {
+        return 'filename';
+    }
+
+    /**
+     * Returns the file name including file extension, or an empty string if the task does not have a filename
+     * @param task
+     * @public
+     */
+    public value(task: Task): string {
+        const filename = task.filename;
+        if (filename === null) {
+            return '';
+        }
+        return filename + '.md';
+    }
+}
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index 7bed0cee..7c3dec3e 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -12,6 +12,7 @@ import { RecurringField } from './Filter/RecurringField';
 import { StatusField } from './Filter/StatusField';
 import { TagsField } from './Filter/TagsField';
 import { BooleanField } from './Filter/BooleanField';
+import { FilenameField } from './Filter/FilenameField';
 
 import type { FilterOrErrorMessage } from './Filter/Filter';
 
@@ -30,6 +31,7 @@ const fieldCreators = [
     () => new HeadingField(),
     () => new ExcludeSubItemsField(),
     () => new BooleanField(),
+    () => new FilenameField(),
 ];
 
 export function parseFilter(filterString: string): FilterOrErrorMessage | null {
diff --git a/src/Task.ts b/src/Task.ts
index 079b8da9..f00ae1e2 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -674,6 +674,9 @@ export class Task {
         return this._urgency;
     }
 
+    /**
+     * Return the name of the file containing the task, with the .md extension removed.
+     */
     public get filename(): string | null {
         const fileNameMatch = this.path.match(/([^/]+)\.md$/);
         if (fileNameMatch !== null) {


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 2578ef3a..951d2e43 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -24,6 +24,7 @@ describe('Query parsing', () => {
         'due before 2021-12-27',
         'due on 2021-12-27',
         'exclude sub-items',
+        'filename includes wibble',
         'happens after 2021-12-27',
         'happens before 2021-12-27',
         'happens on 2021-12-27',
diff --git a/tests/Query/Filter/FilenameField.test.ts b/tests/Query/Filter/FilenameField.test.ts
new file mode 100644
index 00000000..31102afe
--- /dev/null
+++ b/tests/Query/Filter/FilenameField.test.ts
@@ -0,0 +1,90 @@
+import { TaskBuilder } from '../../TestingTools/TaskBuilder';
+import { FilenameField } from '../../../src/Query/Filter/FilenameField';
+import {
+    toBeValid,
+    toMatchTaskWithPath,
+} from '../../CustomMatchers/CustomMatchersForFilters';
+
+expect.extend({
+    toMatchTaskWithPath,
+});
+
+expect.extend({
+    toBeValid,
+});
+
+describe('filename', () => {
+    it('should provide access to the file name with extension', () => {
+        const pathField = new FilenameField();
+
+        const builder = new TaskBuilder();
+
+        expect(pathField.value(builder.path('').build())).toStrictEqual('');
+
+        expect(
+            pathField.value(builder.path('file in root.md').build()),
+        ).toStrictEqual('file in root.md');
+
+        expect(
+            pathField.value(
+                builder.path('directory name/file in sub-directory.md').build(),
+            ),
+        ).toStrictEqual('file in sub-directory.md');
+    });
+});
+
+describe('filename', () => {
+    // Note: We don't need to check all behaviours that are implemented in the base class.
+    // These are minimal tests to confirm that the filters are correctly wired up,
+    // to guard against possible future coding errors.
+
+    it('by filename (includes)', () => {
+        // Arrange
+        const filter = new FilenameField().createFilterOrErrorMessage(
+            'filename includes search_text',
+        );
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).not.toMatchTaskWithPath('');
+        expect(filter).toMatchTaskWithPath('/some/path/SeArch_Text.md');
+        expect(filter).not.toMatchTaskWithPath('/other/search_text/file.md'); // Ignores text in folder names
+    });
+
+    it('by filename (does not include)', () => {
+        // Arrange
+        const filter = new FilenameField().createFilterOrErrorMessage(
+            'filename does not include search_text',
+        );
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).toMatchTaskWithPath('');
+        expect(filter).toMatchTaskWithPath('/other/search_text/file.md'); // Ignores text in folder names
+        expect(filter).not.toMatchTaskWithPath('/SoMe/PaTh/SeArcH_Text.md');
+    });
+
+    it('by filename (regex matches)', () => {
+        // Arrange
+        const filter = new FilenameField().createFilterOrErrorMessage(
+            String.raw`filename regex matches /w.bble/`,
+        );
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).toMatchTaskWithPath('/some/path/wibble.md');
+        expect(filter).not.toMatchTaskWithPath('/some/wibble/filename.md');
+    });
+
+    it('by filename (regex does not match)', () => {
+        // Arrange
+        const filter = new FilenameField().createFilterOrErrorMessage(
+            String.raw`filename regex does not match /w.bble/`,
+        );
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).toMatchTaskWithPath('/some/wobble/path name.md');
+        expect(filter).not.toMatchTaskWithPath('/some/path/wibble.md');
+    });
+});