Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1873 (2023-04-13 06:08:59)

feat: Add 'root' and 'folder' filters
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

## Motivation and Context

Refactoring `Group` methods to `Field` methods need creation of new fields
Part of #920, continuing #1870.

I took the base class for the new Field as `TextField` since it's `grouper()` was already implemented  there

The contents of `groupBy...()` methods were moved to `value()` of the new classed, obviously removing the array in the `return` (It will be added in `TextField.grouper()`). This is a major difference with the #1870 where the methods have been moved to `grouper()` of the existing Field classes.

As for the tests, I reused the tests from `Group.test` and also created new tests to test the `value()` in a simple manner since this is where the change is done.

I see more refactoring to be done in `Group` but I think this PR is already big enough so I'll save that for a later one.

## 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)
- [x] **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:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **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! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] 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 follow 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)


ilandikov: > If you have the time, could you update the summary line of this PR too...

Oops, I got too excited and forgot this one =)

> Take a deep breath.... Here are the instructions:

Love it =)

> you have added support for filtering and sorting of 3 new fields

Oh wow...This was..._simple_ ;)

Ok so I'm missing things tests and docs on filtering and sorting of the new fields?

claremacrae: Hi @ilandikov I think I'm wrong about this having enabled sorting...

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/cfa3f8884be7f19d1df906b68159a7054d28f4ac/src/Query/Filter/TextField.ts#L95-L98

But I think it probably has enabled filtering... Maybe????


ilandikov: Well, it’s a matter of enabling sorting (new information) and writing tests (as discussed already)
claremacrae: Sorry to be the bearer of bad news....

There were no tests for filtering in Root and Folder, so I added tests.

I was unable to get the Root ones to pass, and after some time, added more tests its value() which showed that there is an error in the `RootField.value()`

When fix it, please revert the duplication of `value()` implementation in this class. The grouping code can simply call `this.value()` in this simple case.
ilandikov: > Sorry to be the bearer of bad news....
> 
> There were no tests for filtering in Root and Folder, so I added tests.
> 

That's actually good news =) Bug report on a released feature would be bad news ;)

> I was unable to get the Root ones to pass, and after some time, added more tests its value() which showed that there is an error in the `RootField.value()`

Fixed. Btw are you sure Obsidian would provide roots starting with `/`.

> When fix it, please revert the duplication of `value()` implementation in this class. The grouping code can simply call `this.value()` in this simple case.

Indeed. I did this for the `FolderField` as well.



Patch:
diff --git a/contributing/.obsidian/community-plugins.json b/contributing/.obsidian/community-plugins.json
index 00a19449..2c742012 100644
--- a/contributing/.obsidian/community-plugins.json
+++ b/contributing/.obsidian/community-plugins.json
@@ -4,5 +4,6 @@
   "reveal-active-file-button",
   "table-editor-obsidian",
   "copy-publish-url",
-  "cmdr"
+  "cmdr",
+  "vault-name-status-bar"
 ]
\ No newline at end of file
diff --git a/contributing/.obsidian/plugins/vault-name-status-bar/main.js b/contributing/.obsidian/plugins/vault-name-status-bar/main.js
new file mode 100644
index 00000000..05bd9cf0
--- /dev/null
+++ b/contributing/.obsidian/plugins/vault-name-status-bar/main.js
@@ -0,0 +1,86 @@
+/*
+THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
+if you want to view the source, please visit the github repository of this plugin
+*/
+
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __export = (target, all) => {
+  for (var name in all)
+    __defProp(target, name, { get: all[name], enumerable: true });
+};
+var __copyProps = (to, from, except, desc) => {
+  if (from && typeof from === "object" || typeof from === "function") {
+    for (let key of __getOwnPropNames(from))
+      if (!__hasOwnProp.call(to, key) && key !== except)
+        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+  }
+  return to;
+};
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+
+// src/main.ts
+var main_exports = {};
+__export(main_exports, {
+  default: () => StatusBarVaultName
+});
+module.exports = __toCommonJS(main_exports);
+var import_obsidian = require("obsidian");
+var DEFAULT_SETTINGS = {
+  reducedAtStart: false
+};
+var StatusBarVaultName = class extends import_obsidian.Plugin {
+  async onload() {
+    await this.loadSettings();
+    this.addSettingTab(new Settings(this.app, this));
+    const { workspace } = this.app;
+    const vaultName = this.app.vault.getName();
+    const statusBar = document.querySelector(".status-bar");
+    this.title = document.createElement("div");
+    this.settings.reducedAtStart ? this.title.innerText = "::" : this.title.innerText = vaultName;
+    this.title.classList.add("status-bar-vault-name");
+    statusBar == null ? void 0 : statusBar.prepend(this.title);
+    this.title.addEventListener("click", () => {
+      if (this.title.innerText === vaultName) {
+        this.title.innerText = "::";
+      } else {
+        this.title.innerText = vaultName;
+      }
+    });
+  }
+  onunload() {
+    this.title.detach();
+  }
+  async loadSettings() {
+    this.settings = Object.assign(
+      {},
+      DEFAULT_SETTINGS,
+      await this.loadData()
+    );
+  }
+  async saveSettings() {
+    await this.saveData(this.settings);
+  }
+};
+var Settings = class extends import_obsidian.PluginSettingTab {
+  constructor(app, plugin) {
+    super(app, plugin);
+    this.plugin = plugin;
+  }
+  display() {
+    const { containerEl } = this;
+    containerEl.empty();
+    containerEl.createEl("h2", { text: "Vaut Name in Status Bar" });
+    containerEl.createEl("p", { text: "Click on the vault name or on :: to reduce or enlarge" });
+    new import_obsidian.Setting(containerEl).setName("Reduced at Start").setDesc("Will be reduced by default").addToggle((toggle) => {
+      toggle.setValue(this.plugin.settings.reducedAtStart).onChange((value) => {
+        this.plugin.settings.reducedAtStart = value;
+        this.plugin.saveSettings();
+        this.plugin.settings.reducedAtStart ? this.plugin.title.innerText = "::" : this.plugin.title.innerText = this.app.vault.getName();
+      });
+    });
+  }
+};
+//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL21haW4udHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImltcG9ydCB7IEFwcCwgUGx1Z2luLCBQbHVnaW5TZXR0aW5nVGFiLCBTZXR0aW5nLCBWaWV3IH0gZnJvbSBcIm9ic2lkaWFuXCI7XHJcblxyXG5pbnRlcmZhY2UgU0JWTlNldHRpbmdzIHtcclxuXHRyZWR1Y2VkQXRTdGFydDogYm9vbGVhbjtcclxufVxyXG5cclxuY29uc3QgREVGQVVMVF9TRVRUSU5HUzogU0JWTlNldHRpbmdzID0ge1xyXG5cdHJlZHVjZWRBdFN0YXJ0OiBmYWxzZSxcclxufTtcclxuXHJcbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFN0YXR1c0JhclZhdWx0TmFtZSBleHRlbmRzIFBsdWdpbiB7XHJcblx0c2V0dGluZ3M6IFNCVk5TZXR0aW5nc1xyXG5cdHRpdGxlOiBIVE1MRGl2RWxlbWVudFxyXG5cclxuXHRhc3luYyBvbmxvYWQoKSB7XHJcblx0XHRhd2FpdCB0aGlzLmxvYWRTZXR0aW5ncygpO1xyXG5cdFx0dGhpcy5hZGRTZXR0aW5nVGFiKG5ldyBTZXR0aW5ncyh0aGlzLmFwcCwgdGhpcykpO1xyXG5cdFx0Y29uc3QgeyB3b3Jrc3BhY2UgfSA9IHRoaXMuYXBwO1xyXG5cdFx0Y29uc3QgdmF1bHROYW1lID0gdGhpcy5hcHAudmF1bHQuZ2V0TmFtZSgpO1xyXG5cdFx0Y29uc3Qgc3RhdHVzQmFyID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcignLnN0YXR1cy1iYXInKVxyXG5cdFx0dGhpcy50aXRsZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpO1xyXG5cdFx0dGhpcy5zZXR0aW5ncy5yZWR1Y2VkQXRTdGFydCA/IHRoaXMudGl0bGUuaW5uZXJUZXh0ID0gXCI6OlwiIDogdGhpcy50aXRsZS5pbm5lclRleHQgPSB2YXVsdE5hbWVcclxuXHRcdHRoaXMudGl0bGUuY2xhc3NMaXN0LmFkZChcInN0YXR1cy1iYXItdmF1bHQtbmFtZVwiKTtcclxuXHRcdHN0YXR1c0Jhcj8ucHJlcGVuZCh0aGlzLnRpdGxlKVxyXG5cclxuXHRcdHRoaXMudGl0bGUuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcblx0XHRcdGlmICh0aGlzLnRpdGxlLmlubmVyVGV4dCA9PT0gdmF1bHROYW1lKSB7XHJcblx0XHRcdFx0dGhpcy50aXRsZS5pbm5lclRleHQgPSBcIjo6XCI7XHJcblx0XHRcdH0gZWxzZSB7XHJcblx0XHRcdFx0dGhpcy50aXRsZS5pbm5lclRleHQgPSB2YXVsdE5hbWU7XHJcblx0XHRcdH1cclxuXHRcdH0pO1xyXG5cclxuXHR9XHJcblxyXG5cdG9udW5sb2FkKCkge1xyXG5cdFx0dGhpcy50aXRsZS5kZXRhY2goKVxyXG5cclxuXHR9XHJcblx0YXN5bmMgbG9hZFNldHRpbmdzKCkge1xyXG5cdFx0dGhpcy5zZXR0aW5ncyA9IE9iamVjdC5hc3NpZ24oXHJcblx0XHRcdHt9LFxyXG5cdFx0XHRERUZBVUxUX1NFVFRJTkdTLFxyXG5cdFx0XHRhd2FpdCB0aGlzLmxvYWREYXRhKClcclxuXHRcdCk7XHJcblx0fVxyXG5cclxuXHRhc3luYyBzYXZlU2V0dGluZ3MoKSB7XHJcblx0XHRhd2FpdCB0aGlzLnNhdmVEYXRhKHRoaXMuc2V0dGluZ3MpO1xyXG5cdH1cclxuXHJcbn1cclxuXHJcbmNsYXNzIFNldHRpbmdzIGV4dGVuZHMgUGx1Z2luU2V0dGluZ1RhYiB7XHJcblx0cGx1Z2luOiBTdGF0dXNCYXJWYXVsdE5hbWU7XHJcblxyXG5cdGNvbnN0cnVjdG9yKGFwcDogQXBwLCBwbHVnaW46IFN0YXR1c0JhclZhdWx0TmFtZSkge1xyXG5cdFx0c3VwZXIoYXBwLCBwbHVnaW4pO1xyXG5cdFx0dGhpcy5wbHVnaW4gPSBwbHVnaW47XHJcblx0fVxyXG5cclxuXHRkaXNwbGF5KCk6IHZvaWQge1xyXG5cdFx0Y29uc3QgeyBjb250YWluZXJFbCB9ID0gdGhpcztcclxuXHJcblx0XHRjb250YWluZXJFbC5lbXB0eSgpO1xyXG5cclxuXHRcdGNvbnRhaW5lckVsLmNyZWF0ZUVsKCdoMicsIHsgdGV4dDogJ1ZhdXQgTmFtZSBpbiBTdGF0dXMgQmFyJyB9KTtcclxuXHRcdGNvbnRhaW5lckVsLmNyZWF0ZUVsKCdwJywgeyB0ZXh0OiAnQ2xpY2sgb24gdGhlIHZhdWx0IG5hbWUgb3Igb24gOjogdG8gcmVkdWNlIG9yIGVubGFyZ2UnIH0pO1xyXG5cclxuXHRcdG5ldyBTZXR0aW5nKGNvbnRhaW5lckVsKVxyXG5cdFx0XHQuc2V0TmFtZShcIlJlZHVjZWQgYXQgU3RhcnRcIilcclxuXHRcdFx0LnNldERlc2MoXCJXaWxsIGJlIHJlZHVjZWQgYnkgZGVmYXVsdFwiKVxyXG5cdFx0XHQuYWRkVG9nZ2xlKCh0b2dnbGUpID0+IHtcclxuXHRcdFx0XHR0b2dnbGVcclxuXHRcdFx0XHRcdC5zZXRWYWx1ZSh0aGlzLnBsdWdpbi5zZXR0aW5ncy5yZWR1Y2VkQXRTdGFydClcclxuXHRcdFx0XHRcdC5vbkNoYW5nZSgodmFsdWUpID0+IHtcclxuXHRcdFx0XHRcdFx0dGhpcy5wbHVnaW4uc2V0dGluZ3MucmVkdWNlZEF0U3RhcnQgPSB2YWx1ZTtcclxuXHRcdFx0XHRcdFx0dGhpcy5wbHVnaW4uc2F2ZVNldHRpbmdzKCk7XHJcblx0XHRcdFx0XHRcdHRoaXMucGx1Z2luLnNldHRpbmdzLnJlZHVjZWRBdFN0YXJ0XHJcblx0XHRcdFx0XHRcdFx0PyAodGhpcy5wbHVnaW4udGl0bGUuaW5uZXJUZXh0ID0gXCI6OlwiKVxyXG5cdFx0XHRcdFx0XHRcdDogKHRoaXMucGx1Z2luLnRpdGxlLmlubmVyVGV4dCA9IHRoaXMuYXBwLnZhdWx0LmdldE5hbWUoKSk7XHJcblx0XHRcdFx0XHR9KTtcclxuXHRcdFx0fSk7XHJcblx0fVxyXG59XHJcblxyXG4iXSwKICAibWFwcGluZ3MiOiAiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUEsc0JBQTZEO0FBTTdELElBQU0sbUJBQWlDO0FBQUEsRUFDdEMsZ0JBQWdCO0FBQ2pCO0FBRUEsSUFBcUIscUJBQXJCLGNBQWdELHVCQUFPO0FBQUEsRUFJdEQsTUFBTSxTQUFTO0FBQ2QsVUFBTSxLQUFLLGFBQWE7QUFDeEIsU0FBSyxjQUFjLElBQUksU0FBUyxLQUFLLEtBQUssSUFBSSxDQUFDO0FBQy9DLFVBQU0sRUFBRSxVQUFVLElBQUksS0FBSztBQUMzQixVQUFNLFlBQVksS0FBSyxJQUFJLE1BQU0sUUFBUTtBQUN6QyxVQUFNLFlBQVksU0FBUyxjQUFjLGFBQWE7QUFDdEQsU0FBSyxRQUFRLFNBQVMsY0FBYyxLQUFLO0FBQ3pDLFNBQUssU0FBUyxpQkFBaUIsS0FBSyxNQUFNLFlBQVksT0FBTyxLQUFLLE1BQU0sWUFBWTtBQUNwRixTQUFLLE1BQU0sVUFBVSxJQUFJLHVCQUF1QjtBQUNoRCwyQ0FBVyxRQUFRLEtBQUs7QUFFeEIsU0FBSyxNQUFNLGlCQUFpQixTQUFTLE1BQU07QUFDMUMsVUFBSSxLQUFLLE1BQU0sY0FBYyxXQUFXO0FBQ3ZDLGFBQUssTUFBTSxZQUFZO0FBQUEsTUFDeEIsT0FBTztBQUNOLGFBQUssTUFBTSxZQUFZO0FBQUEsTUFDeEI7QUFBQSxJQUNELENBQUM7QUFBQSxFQUVGO0FBQUEsRUFFQSxXQUFXO0FBQ1YsU0FBSyxNQUFNLE9BQU87QUFBQSxFQUVuQjtBQUFBLEVBQ0EsTUFBTSxlQUFlO0FBQ3BCLFNBQUssV0FBVyxPQUFPO0FBQUEsTUFDdEIsQ0FBQztBQUFBLE1BQ0Q7QUFBQSxNQUNBLE1BQU0sS0FBSyxTQUFTO0FBQUEsSUFDckI7QUFBQSxFQUNEO0FBQUEsRUFFQSxNQUFNLGVBQWU7QUFDcEIsVUFBTSxLQUFLLFNBQVMsS0FBSyxRQUFRO0FBQUEsRUFDbEM7QUFFRDtBQUVBLElBQU0sV0FBTixjQUF1QixpQ0FBaUI7QUFBQSxFQUd2QyxZQUFZLEtBQVUsUUFBNEI7QUFDakQsVUFBTSxLQUFLLE1BQU07QUFDakIsU0FBSyxTQUFTO0FBQUEsRUFDZjtBQUFBLEVBRUEsVUFBZ0I7QUFDZixVQUFNLEVBQUUsWUFBWSxJQUFJO0FBRXhCLGdCQUFZLE1BQU07QUFFbEIsZ0JBQVksU0FBUyxNQUFNLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUM5RCxnQkFBWSxTQUFTLEtBQUssRUFBRSxNQUFNLHdEQUF3RCxDQUFDO0FBRTNGLFFBQUksd0JBQVEsV0FBVyxFQUNyQixRQUFRLGtCQUFrQixFQUMxQixRQUFRLDRCQUE0QixFQUNwQyxVQUFVLENBQUMsV0FBVztBQUN0QixhQUNFLFNBQVMsS0FBSyxPQUFPLFNBQVMsY0FBYyxFQUM1QyxTQUFTLENBQUMsVUFBVTtBQUNwQixhQUFLLE9BQU8sU0FBUyxpQkFBaUI7QUFDdEMsYUFBSyxPQUFPLGFBQWE7QUFDekIsYUFBSyxPQUFPLFNBQVMsaUJBQ2pCLEtBQUssT0FBTyxNQUFNLFlBQVksT0FDOUIsS0FBSyxPQUFPLE1BQU0sWUFBWSxLQUFLLElBQUksTUFBTSxRQUFRO0FBQUEsTUFDMUQsQ0FBQztBQUFBLElBQ0gsQ0FBQztBQUFBLEVBQ0g7QUFDRDsiLAogICJuYW1lcyI6IFtdCn0K
diff --git a/contributing/.obsidian/plugins/vault-name-status-bar/manifest.json b/contributing/.obsidian/plugins/vault-name-status-bar/manifest.json
new file mode 100644
index 00000000..2ee861a8
--- /dev/null
+++ b/contributing/.obsidian/plugins/vault-name-status-bar/manifest.json
@@ -0,0 +1,10 @@
+{
+	"id": "vault-name-status-bar",
+	"name": "Vault name in status bar",
+	"version": "1.0.1",
+	"minAppVersion": "1.1.0",
+	"description": "Vault name in status bar. can be reduced to ::",
+	"author": "1C0D",
+	"authorUrl": "",
+	"isDesktopOnly": true
+}
diff --git a/contributing/.obsidian/plugins/vault-name-status-bar/styles.css b/contributing/.obsidian/plugins/vault-name-status-bar/styles.css
new file mode 100644
index 00000000..8a56f1a7
--- /dev/null
+++ b/contributing/.obsidian/plugins/vault-name-status-bar/styles.css
@@ -0,0 +1,5 @@
+.status-bar-vault-name {
+  font-size: 110%;
+  font-weight: bold;
+  color: rgb(0, 115, 255);
+}
\ No newline at end of file
diff --git a/contributing/Dependencies/About Dependencies.md b/contributing/Dependencies/About Dependencies.md
index 3a68f9ef..0ac220f8 100644
--- a/contributing/Dependencies/About Dependencies.md
+++ b/contributing/Dependencies/About Dependencies.md
@@ -6,7 +6,8 @@ Dependencies for the plugin are set using the two files:
 - [yarn.lock](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/yarn.lock) (machine-generated)
 
 After any change to dependencies in `package.json`, run `yarn` to update the `yarn.lock` file and commit the changes in both files.
-If you see a warning from `yarn` about a **missing peer dependency that mentions `obsidian` or `@codemirror`, you can safely ignore it**. Other yarn messages should likely be resolved before commit.
+
+If you see a warning from `yarn` about a **unmet peer dependency that mentions `obsidian` or `@codemirror`, you can safely ignore it**. Other yarn messages should likely be resolved before commit.
 
 Code changes that also involve dependency changes may require additional testing and review.
 Please only change [package.json](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/package.json) if necessary for your contribution.
diff --git a/contributing/Dependencies/Overview of dependencies and `package.json`.md b/contributing/Dependencies/Overview of dependencies and `package.json`.md
index aefa75e1..14e5e262 100644
--- a/contributing/Dependencies/Overview of dependencies and `package.json`.md
+++ b/contributing/Dependencies/Overview of dependencies and `package.json`.md
@@ -34,3 +34,16 @@ Therefore, "devDependency" vs. "dependency" separation is not a sufficient indic
 needs manual "[[How do I smoke-test the Tasks plugin|smoke testing]]" of runtime behavior.
 
 See also [[Thought-Process for Deciding Whether a Dependency Needs Manual Testing]].
+
+### Obsidian's current dependencies
+
+As of 2023-04-16, the Obsidian team reported that Obsidian team [reported that](https://discord.com/channels/686053708261228577/840286264964022302/1097141108972212274) Obsidian's codemirror dependency versions are:
+
+```yaml
+"@codemirror/commands": "6.2.1",
+"@codemirror/language": "https://github.com/lishid/cm-language",
+"@codemirror/lint": "6.1.1",
+"@codemirror/search": "6.2.3",
+"@codemirror/state": "6.2.0",
+"@codemirror/view": "https://github.com/lishid/cm-view",
+```
diff --git a/docs/.obsidian/community-plugins.json b/docs/.obsidian/community-plugins.json
index d867b29c..73c8a7c1 100644
--- a/docs/.obsidian/community-plugins.json
+++ b/docs/.obsidian/community-plugins.json
@@ -6,5 +6,6 @@
   "obsidian-tasks-plugin",
   "reveal-active-file-button",
   "templater-obsidian",
-  "tag-wrangler"
+  "tag-wrangler",
+  "vault-name-status-bar"
 ]
\ No newline at end of file
diff --git a/docs/.obsidian/hotkeys.json b/docs/.obsidian/hotkeys.json
index 9e26dfee..38cf39ba 100644
--- a/docs/.obsidian/hotkeys.json
+++ b/docs/.obsidian/hotkeys.json
@@ -1,1 +1,17 @@
-{}
\ No newline at end of file
+{
+  "editor:toggle-comments": [],
+  "markdown:toggle-preview": [
+    {
+      "modifiers": [
+        "Mod"
+      ],
+      "key": "E"
+    },
+    {
+      "modifiers": [
+        "Mod"
+      ],
+      "key": "/"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/docs/.obsidian/plugins/vault-name-status-bar/main.js b/docs/.obsidian/plugins/vault-name-status-bar/main.js
new file mode 100644
index 00000000..05bd9cf0
--- /dev/null
+++ b/docs/.obsidian/plugins/vault-name-status-bar/main.js
@@ -0,0 +1,86 @@
+/*
+THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
+if you want to view the source, please visit the github repository of this plugin
+*/
+
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __export = (target, all) => {
+  for (var name in all)
+    __defProp(target, name, { get: all[name], enumerable: true });
+};
+var __copyProps = (to, from, except, desc) => {
+  if (from && typeof from === "object" || typeof from === "function") {
+    for (let key of __getOwnPropNames(from))
+      if (!__hasOwnProp.call(to, key) && key !== except)
+        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+  }
+  return to;
+};
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+
+// src/main.ts
+var main_exports = {};
+__export(main_exports, {
+  default: () => StatusBarVaultName
+});
+module.exports = __toCommonJS(main_exports);
+var import_obsidian = require("obsidian");
+var DEFAULT_SETTINGS = {
+  reducedAtStart: false
+};
+var StatusBarVaultName = class extends import_obsidian.Plugin {
+  async onload() {
+    await this.loadSettings();
+    this.addSettingTab(new Settings(this.app, this));
+    const { workspace } = this.app;
+    const vaultName = this.app.vault.getName();
+    const statusBar = document.querySelector(".status-bar");
+    this.title = document.createElement("div");
+    this.settings.reducedAtStart ? this.title.innerText = "::" : this.title.innerText = vaultName;
+    this.title.classList.add("status-bar-vault-name");
+    statusBar == null ? void 0 : statusBar.prepend(this.title);
+    this.title.addEventListener("click", () => {
+      if (this.title.innerText === vaultName) {
+        this.title.innerText = "::";
+      } else {
+        this.title.innerText = vaultName;
+      }
+    });
+  }
+  onunload() {
+    this.title.detach();
+  }
+  async loadSettings() {
+    this.settings = Object.assign(
+      {},
+      DEFAULT_SETTINGS,
+      await this.loadData()
+    );
+  }
+  async saveSettings() {
+    await this.saveData(this.settings);
+  }
+};
+var Settings = class extends import_obsidian.PluginSettingTab {
+  constructor(app, plugin) {
+    super(app, plugin);
+    this.plugin = plugin;
+  }
+  display() {
+    const { containerEl } = this;
+    containerEl.empty();
+    containerEl.createEl("h2", { text: "Vaut Name in Status Bar" });
+    containerEl.createEl("p", { text: "Click on the vault name or on :: to reduce or enlarge" });
+    new import_obsidian.Setting(containerEl).setName("Reduced at Start").setDesc("Will be reduced by default").addToggle((toggle) => {
+      toggle.setValue(this.plugin.settings.reducedAtStart).onChange((value) => {
+        this.plugin.settings.reducedAtStart = value;
+        this.plugin.saveSettings();
+        this.plugin.settings.reducedAtStart ? this.plugin.title.innerText = "::" : this.plugin.title.innerText = this.app.vault.getName();
+      });
+    });
+  }
+};
+//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL21haW4udHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImltcG9ydCB7IEFwcCwgUGx1Z2luLCBQbHVnaW5TZXR0aW5nVGFiLCBTZXR0aW5nLCBWaWV3IH0gZnJvbSBcIm9ic2lkaWFuXCI7XHJcblxyXG5pbnRlcmZhY2UgU0JWTlNldHRpbmdzIHtcclxuXHRyZWR1Y2VkQXRTdGFydDogYm9vbGVhbjtcclxufVxyXG5cclxuY29uc3QgREVGQVVMVF9TRVRUSU5HUzogU0JWTlNldHRpbmdzID0ge1xyXG5cdHJlZHVjZWRBdFN0YXJ0OiBmYWxzZSxcclxufTtcclxuXHJcbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFN0YXR1c0JhclZhdWx0TmFtZSBleHRlbmRzIFBsdWdpbiB7XHJcblx0c2V0dGluZ3M6IFNCVk5TZXR0aW5nc1xyXG5cdHRpdGxlOiBIVE1MRGl2RWxlbWVudFxyXG5cclxuXHRhc3luYyBvbmxvYWQoKSB7XHJcblx0XHRhd2FpdCB0aGlzLmxvYWRTZXR0aW5ncygpO1xyXG5cdFx0dGhpcy5hZGRTZXR0aW5nVGFiKG5ldyBTZXR0aW5ncyh0aGlzLmFwcCwgdGhpcykpO1xyXG5cdFx0Y29uc3QgeyB3b3Jrc3BhY2UgfSA9IHRoaXMuYXBwO1xyXG5cdFx0Y29uc3QgdmF1bHROYW1lID0gdGhpcy5hcHAudmF1bHQuZ2V0TmFtZSgpO1xyXG5cdFx0Y29uc3Qgc3RhdHVzQmFyID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcignLnN0YXR1cy1iYXInKVxyXG5cdFx0dGhpcy50aXRsZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpO1xyXG5cdFx0dGhpcy5zZXR0aW5ncy5yZWR1Y2VkQXRTdGFydCA/IHRoaXMudGl0bGUuaW5uZXJUZXh0ID0gXCI6OlwiIDogdGhpcy50aXRsZS5pbm5lclRleHQgPSB2YXVsdE5hbWVcclxuXHRcdHRoaXMudGl0bGUuY2xhc3NMaXN0LmFkZChcInN0YXR1cy1iYXItdmF1bHQtbmFtZVwiKTtcclxuXHRcdHN0YXR1c0Jhcj8ucHJlcGVuZCh0aGlzLnRpdGxlKVxyXG5cclxuXHRcdHRoaXMudGl0bGUuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcblx0XHRcdGlmICh0aGlzLnRpdGxlLmlubmVyVGV4dCA9PT0gdmF1bHROYW1lKSB7XHJcblx0XHRcdFx0dGhpcy50aXRsZS5pbm5lclRleHQgPSBcIjo6XCI7XHJcblx0XHRcdH0gZWxzZSB7XHJcblx0XHRcdFx0dGhpcy50aXRsZS5pbm5lclRleHQgPSB2YXVsdE5hbWU7XHJcblx0XHRcdH1cclxuXHRcdH0pO1xyXG5cclxuXHR9XHJcblxyXG5cdG9udW5sb2FkKCkge1xyXG5cdFx0dGhpcy50aXRsZS5kZXRhY2goKVxyXG5cclxuXHR9XHJcblx0YXN5bmMgbG9hZFNldHRpbmdzKCkge1xyXG5cdFx0dGhpcy5zZXR0aW5ncyA9IE9iamVjdC5hc3NpZ24oXHJcblx0XHRcdHt9LFxyXG5cdFx0XHRERUZBVUxUX1NFVFRJTkdTLFxyXG5cdFx0XHRhd2FpdCB0aGlzLmxvYWREYXRhKClcclxuXHRcdCk7XHJcblx0fVxyXG5cclxuXHRhc3luYyBzYXZlU2V0dGluZ3MoKSB7XHJcblx0XHRhd2FpdCB0aGlzLnNhdmVEYXRhKHRoaXMuc2V0dGluZ3MpO1xyXG5cdH1cclxuXHJcbn1cclxuXHJcbmNsYXNzIFNldHRpbmdzIGV4dGVuZHMgUGx1Z2luU2V0dGluZ1RhYiB7XHJcblx0cGx1Z2luOiBTdGF0dXNCYXJWYXVsdE5hbWU7XHJcblxyXG5cdGNvbnN0cnVjdG9yKGFwcDogQXBwLCBwbHVnaW46IFN0YXR1c0JhclZhdWx0TmFtZSkge1xyXG5cdFx0c3VwZXIoYXBwLCBwbHVnaW4pO1xyXG5cdFx0dGhpcy5wbHVnaW4gPSBwbHVnaW47XHJcblx0fVxyXG5cclxuXHRkaXNwbGF5KCk6IHZvaWQge1xyXG5cdFx0Y29uc3QgeyBjb250YWluZXJFbCB9ID0gdGhpcztcclxuXHJcblx0XHRjb250YWluZXJFbC5lbXB0eSgpO1xyXG5cclxuXHRcdGNvbnRhaW5lckVsLmNyZWF0ZUVsKCdoMicsIHsgdGV4dDogJ1ZhdXQgTmFtZSBpbiBTdGF0dXMgQmFyJyB9KTtcclxuXHRcdGNvbnRhaW5lckVsLmNyZWF0ZUVsKCdwJywgeyB0ZXh0OiAnQ2xpY2sgb24gdGhlIHZhdWx0IG5hbWUgb3Igb24gOjogdG8gcmVkdWNlIG9yIGVubGFyZ2UnIH0pO1xyXG5cclxuXHRcdG5ldyBTZXR0aW5nKGNvbnRhaW5lckVsKVxyXG5cdFx0XHQuc2V0TmFtZShcIlJlZHVjZWQgYXQgU3RhcnRcIilcclxuXHRcdFx0LnNldERlc2MoXCJXaWxsIGJlIHJlZHVjZWQgYnkgZGVmYXVsdFwiKVxyXG5cdFx0XHQuYWRkVG9nZ2xlKCh0b2dnbGUpID0+IHtcclxuXHRcdFx0XHR0b2dnbGVcclxuXHRcdFx0XHRcdC5zZXRWYWx1ZSh0aGlzLnBsdWdpbi5zZXR0aW5ncy5yZWR1Y2VkQXRTdGFydClcclxuXHRcdFx0XHRcdC5vbkNoYW5nZSgodmFsdWUpID0+IHtcclxuXHRcdFx0XHRcdFx0dGhpcy5wbHVnaW4uc2V0dGluZ3MucmVkdWNlZEF0U3RhcnQgPSB2YWx1ZTtcclxuXHRcdFx0XHRcdFx0dGhpcy5wbHVnaW4uc2F2ZVNldHRpbmdzKCk7XHJcblx0XHRcdFx0XHRcdHRoaXMucGx1Z2luLnNldHRpbmdzLnJlZHVjZWRBdFN0YXJ0XHJcblx0XHRcdFx0XHRcdFx0PyAodGhpcy5wbHVnaW4udGl0bGUuaW5uZXJUZXh0ID0gXCI6OlwiKVxyXG5cdFx0XHRcdFx0XHRcdDogKHRoaXMucGx1Z2luLnRpdGxlLmlubmVyVGV4dCA9IHRoaXMuYXBwLnZhdWx0LmdldE5hbWUoKSk7XHJcblx0XHRcdFx0XHR9KTtcclxuXHRcdFx0fSk7XHJcblx0fVxyXG59XHJcblxyXG4iXSwKICAibWFwcGluZ3MiOiAiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUEsc0JBQTZEO0FBTTdELElBQU0sbUJBQWlDO0FBQUEsRUFDdEMsZ0JBQWdCO0FBQ2pCO0FBRUEsSUFBcUIscUJBQXJCLGNBQWdELHVCQUFPO0FBQUEsRUFJdEQsTUFBTSxTQUFTO0FBQ2QsVUFBTSxLQUFLLGFBQWE7QUFDeEIsU0FBSyxjQUFjLElBQUksU0FBUyxLQUFLLEtBQUssSUFBSSxDQUFDO0FBQy9DLFVBQU0sRUFBRSxVQUFVLElBQUksS0FBSztBQUMzQixVQUFNLFlBQVksS0FBSyxJQUFJLE1BQU0sUUFBUTtBQUN6QyxVQUFNLFlBQVksU0FBUyxjQUFjLGFBQWE7QUFDdEQsU0FBSyxRQUFRLFNBQVMsY0FBYyxLQUFLO0FBQ3pDLFNBQUssU0FBUyxpQkFBaUIsS0FBSyxNQUFNLFlBQVksT0FBTyxLQUFLLE1BQU0sWUFBWTtBQUNwRixTQUFLLE1BQU0sVUFBVSxJQUFJLHVCQUF1QjtBQUNoRCwyQ0FBVyxRQUFRLEtBQUs7QUFFeEIsU0FBSyxNQUFNLGlCQUFpQixTQUFTLE1BQU07QUFDMUMsVUFBSSxLQUFLLE1BQU0sY0FBYyxXQUFXO0FBQ3ZDLGFBQUssTUFBTSxZQUFZO0FBQUEsTUFDeEIsT0FBTztBQUNOLGFBQUssTUFBTSxZQUFZO0FBQUEsTUFDeEI7QUFBQSxJQUNELENBQUM7QUFBQSxFQUVGO0FBQUEsRUFFQSxXQUFXO0FBQ1YsU0FBSyxNQUFNLE9BQU87QUFBQSxFQUVuQjtBQUFBLEVBQ0EsTUFBTSxlQUFlO0FBQ3BCLFNBQUssV0FBVyxPQUFPO0FBQUEsTUFDdEIsQ0FBQztBQUFBLE1BQ0Q7QUFBQSxNQUNBLE1BQU0sS0FBSyxTQUFTO0FBQUEsSUFDckI7QUFBQSxFQUNEO0FBQUEsRUFFQSxNQUFNLGVBQWU7QUFDcEIsVUFBTSxLQUFLLFNBQVMsS0FBSyxRQUFRO0FBQUEsRUFDbEM7QUFFRDtBQUVBLElBQU0sV0FBTixjQUF1QixpQ0FBaUI7QUFBQSxFQUd2QyxZQUFZLEtBQVUsUUFBNEI7QUFDakQsVUFBTSxLQUFLLE1BQU07QUFDakIsU0FBSyxTQUFTO0FBQUEsRUFDZjtBQUFBLEVBRUEsVUFBZ0I7QUFDZixVQUFNLEVBQUUsWUFBWSxJQUFJO0FBRXhCLGdCQUFZLE1BQU07QUFFbEIsZ0JBQVksU0FBUyxNQUFNLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUM5RCxnQkFBWSxTQUFTLEtBQUssRUFBRSxNQUFNLHdEQUF3RCxDQUFDO0FBRTNGLFFBQUksd0JBQVEsV0FBVyxFQUNyQixRQUFRLGtCQUFrQixFQUMxQixRQUFRLDRCQUE0QixFQUNwQyxVQUFVLENBQUMsV0FBVztBQUN0QixhQUNFLFNBQVMsS0FBSyxPQUFPLFNBQVMsY0FBYyxFQUM1QyxTQUFTLENBQUMsVUFBVTtBQUNwQixhQUFLLE9BQU8sU0FBUyxpQkFBaUI7QUFDdEMsYUFBSyxPQUFPLGFBQWE7QUFDekIsYUFBSyxPQUFPLFNBQVMsaUJBQ2pCLEtBQUssT0FBTyxNQUFNLFlBQVksT0FDOUIsS0FBSyxPQUFPLE1BQU0sWUFBWSxLQUFLLElBQUksTUFBTSxRQUFRO0FBQUEsTUFDMUQsQ0FBQztBQUFBLElBQ0gsQ0FBQztBQUFBLEVBQ0g7QUFDRDsiLAogICJuYW1lcyI6IFtdCn0K
diff --git a/docs/.obsidian/plugins/vault-name-status-bar/manifest.json b/docs/.obsidian/plugins/vault-name-status-bar/manifest.json
new file mode 100644
index 00000000..2ee861a8
--- /dev/null
+++ b/docs/.obsidian/plugins/vault-name-status-bar/manifest.json
@@ -0,0 +1,10 @@
+{
+	"id": "vault-name-status-bar",
+	"name": "Vault name in status bar",
+	"version": "1.0.1",
+	"minAppVersion": "1.1.0",
+	"description": "Vault name in status bar. can be reduced to ::",
+	"author": "1C0D",
+	"authorUrl": "",
+	"isDesktopOnly": true
+}
diff --git a/docs/.obsidian/plugins/vault-name-status-bar/styles.css b/docs/.obsidian/plugins/vault-name-status-bar/styles.css
new file mode 100644
index 00000000..8a56f1a7
--- /dev/null
+++ b/docs/.obsidian/plugins/vault-name-status-bar/styles.css
@@ -0,0 +1,5 @@
+.status-bar-vault-name {
+  font-size: 110%;
+  font-weight: bold;
+  color: rgb(0, 115, 255);
+}
\ No newline at end of file
diff --git a/docs/Advanced/Daily Agenda.md b/docs/Advanced/Daily Agenda.md
index 9db3a9e8..72a5620b 100644
--- a/docs/Advanced/Daily Agenda.md
+++ b/docs/Advanced/Daily Agenda.md
@@ -7,7 +7,7 @@ publish: true
 <span class="related-pages">#plugin/calendar #plugin/periodic-notes</span>
 
 Using the default `Daily-notes` plugin, **templates** with syntax like
-{% raw %}`{{date+14d:YYYY-MM-DD}}`{% endraw %} won't load dates properly. Nevertheless, **Liam Cain**,
+`{{date+14d:YYYY-MM-DD}}` won't load dates properly. Nevertheless, **Liam Cain**,
 author of both the [Calendar Plugin](https://github.com/liamcain/obsidian-calendar-plugin)
 and [Periodic Notes Plugin](https://github.com/liamcain/obsidian-periodic-notes), has
 written code to create new daily notes (using both plugins) using the `date+Xd` format.
@@ -17,10 +17,10 @@ make sure new notes are created via one of these two plugins, and not `Daily-not
 - **Calendar Plugin**: Just tap the day on the Calendar UI and a new daily note will be created
 - **Periodic Notes Plugin**: Install, migrate from `Daily-notes` if needed, and tap the new `Open Today` ribbon on the left-side dock. Below is an example if today was August 14, 2021.
 
-| | Daily Notes | Calendar | Periodic Notes |
-|-|-------------|----------|----------------|
-| template syntax | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` |
-| output | `due on {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}` | `due on 2021-08-28` | `due on 2021-08-28` |
+|                 | Daily Notes                      | Calendar                         | Periodic Notes                   |
+| --------------- | -------------------------------- | -------------------------------- | -------------------------------- |
+| template syntax | `due on {{date+14d:YYYY-MM-DD}}` | `due on {{date+14d:YYYY-MM-DD}}` | `due on {{date+14d:YYYY-MM-DD}}` |
+| output          | `due on {{date+14d:YYYY-MM-DD}}` | `due on 2021-08-28`              | `due on 2021-08-28`              |
 
 ## Example Daily Agenda **template**
 
@@ -28,20 +28,20 @@ make sure new notes are created via one of these two plugins, and not `Daily-not
     ### Overdue
     ```tasks
     not done
-    due before {% raw %}{{date:YYYY-MM-DD}}{% endraw %}
+    due before {{date:YYYY-MM-DD}}
     ```
 
     ### Due today
     ```tasks
     not done
-    due on {% raw %}{{date:YYYY-MM-DD}}{% endraw %}
+    due on {{date:YYYY-MM-DD}}
     ```
 
     ### Due in the next two weeks
     ```tasks
     not done
-    due after {% raw %}{{date:YYYY-MM-DD}}{% endraw %}
-    due before {% raw %}{{date+14d:YYYY-MM-DD}}{% endraw %}
+    due after {{date:YYYY-MM-DD}}
+    due before {{date+14d:YYYY-MM-DD}}
     ```
 
     ### No due date
@@ -52,5 +52,5 @@ make sure new notes are created via one of these two plugins, and not `Daily-not
 
     ### Done today
     ```tasks
-    done on {% raw %}{{date:YYYY-MM-DD}}{% endraw %}
+    done on {{date:YYYY-MM-DD}}
     ```
diff --git a/docs/Advanced/Quickadd.md b/docs/Advanced/Quickadd.md
index f4785130..dafa0c55 100644
--- a/docs/Advanced/Quickadd.md
+++ b/docs/Advanced/Quickadd.md
@@ -12,7 +12,7 @@ Additional to the official command to create a task, you can use a quickadd comm
 For example:
 
 ```markdown
-{% raw %}#task {{VALUE:task name}} ⏰ {{VDATE:reminder date and time,YYYY-MM-DD HH:mm}} {{VALUE:⏫,🔼,🔽, }} 🔁 {{VALUE:recurrence}} 🛫 {{VDATE:start date,YYYY-MM-DD}} ⏳ {{VDATE:scheduled date,YYYY-MM-DD}} 📅 {{VDATE:due date,YYYY-MM-DD}}{% endraw %}
+#task {{VALUE:task name}} ⏰ {{VDATE:reminder date and time,YYYY-MM-DD HH:mm}} {{VALUE:⏫,🔼,🔽, }} 🔁 {{VALUE:recurrence}} 🛫 {{VDATE:start date,YYYY-MM-DD}} ⏳ {{VDATE:scheduled date,YYYY-MM-DD}} 📅 {{VDATE:due date,YYYY-MM-DD}}
 ```
 
 You can remove/leave some fields to make different types of tasks. And each one can have its own command.
@@ -21,7 +21,7 @@ You can remove/leave some fields to make different types of tasks. And each one
 
 Task with due date only:
 
-`{% raw %}#task {{VALUE:task name}} 📅 {{VDATE:due date,YYYY-MM-DD}}{% endraw %}`
+`#task {{VALUE:task name}} 📅 {{VDATE:due date,YYYY-MM-DD}}`
 
 <video controls width="100%">
     <source src="https://user-images.githubusercontent.com/38974541/143467768-cf183171-296c-4229-81ca-a8f820b7a66e.mov" />
@@ -31,7 +31,7 @@ Task with due date only:
 
 Task with priority and reminder date and due date:
 
-`{% raw %}#task {{VALUE:task name}} ⏰ {{VDATE:reminder date and time,YYYY-MM-DD HH:mm}} {{VALUE:⏫,🔼,🔽, }} 📅 {{VDATE:due date,YYYY-MM-DD}}{% endraw %}`
+`#task {{VALUE:task name}} ⏰ {{VDATE:reminder date and time,YYYY-MM-DD HH:mm}} {{VALUE:⏫,🔼,🔽, }} 📅 {{VDATE:due date,YYYY-MM-DD}}`
 
 <video controls width="100%">
     <source src="https://user-images.githubusercontent.com/38974541/143468599-ae598f7d-cc84-4fc9-8293-eae72cf81f8a.mov" />
@@ -41,7 +41,7 @@ Task with priority and reminder date and due date:
 
 Task with recurrence and scheduled date and start date:
 
-`{% raw %}#task {{VALUE:task name}} 🔁 {{VALUE:recurrence}} 🛫 {{VDATE:start date,YYYY-MM-DD}} ⏳ {{VDATE:scheduled date,YYYY-MM-DD}}{% endraw %}`
+`#task {{VALUE:task name}} 🔁 {{VALUE:recurrence}} 🛫 {{VDATE:start date,YYYY-MM-DD}} ⏳ {{VDATE:scheduled date,YYYY-MM-DD}}`
 
 <video controls width="100%">
     <source src="https://user-images.githubusercontent.com/38974541/143468440-c83b5f91-c923-4f30-9c52-7c69e64978c9.mov" />
@@ -61,5 +61,5 @@ Then you can give them the same name, this way you'll write the date only once a
 Here is the format for the current example:
 
 ```markdown
-{% raw %}#task {{VALUE:task name}} ⏰ {{VDATE:same date,YYYY-MM-DD}} {{VDATE:time,HH:mm}} 📅 {{VDATE:same date,YYYY-MM-DD}}{% endraw %}
+#task {{VALUE:task name}} ⏰ {{VDATE:same date,YYYY-MM-DD}} {{VDATE:time,HH:mm}} 📅 {{VDATE:same date,YYYY-MM-DD}}
 ```
diff --git a/docs/Advanced/Styling.md b/docs/Advanced/Styling.md
index e29e65a3..fd145e13 100644
--- a/docs/Advanced/Styling.md
+++ b/docs/Advanced/Styling.md
@@ -140,7 +140,7 @@ The following additional components have the following classes:
 | Class                          | Usage                                                                                                           |
 | ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
 | plugin-tasks-query-explanation | This is applied to the PRE showing the query's explanation when the `explain` instruction is used.              |
-| tasks-backlink                 | This is applied to the SPAN that wraps the backlink if displayed on the task.                                   |
+| tasks-backlink                 | This is applied to the SPAN that wraps the [[Backlinks|backlink]] if displayed on the task.                                   |
 | tasks-edit                     | This is applied to the SPAN that wraps the edit button/icon shown next to the task that opens the task edit UI. |
 | tasks-urgency                  | This is applied to the SPAN that wraps the urgency score if displayed on the task.                              |
 | tasks-group-heading            | This is applied to H4, H5 and H6 group headings                                                                 |
diff --git a/docs/Getting Started/Backlinks.md b/docs/Getting Started/Backlinks.md
new file mode 100644
index 00000000..b0384e52
--- /dev/null
+++ b/docs/Getting Started/Backlinks.md
@@ -0,0 +1,45 @@
+---
+publish: true
+alias:
+- Backlink
+---
+
+# Backlinks
+
+<span class="related-pages">#feature/backlinks</span>
+
+## What are backlinks?
+
+In Tasks search results, by default each task is displayed with its filename,
+and the name of the previous heading, for example `(ACME > Steps to world domination)`.
+This is called a **backlink**.
+
+This screenshot shows what this might look like, with some sample data:
+
+![Tasks with vanilla backlink styles](../images/backlinks-default-style.png)
+
+If the filename and previous heading are identical, or if there is no previous heading, only the filename is shown.
+
+## Using backlinks for navigation
+
+You can click on a backlink to navigate directly to the task's source line.
+
+> [!Tip]
+> This honours the standard Obsidian keyboard modifiers used when clicking on internal links, to control how the note is opened (Navigate, New Tab, New Tab Group, New Window).
+>
+> See the table in the Tabs section of the [Obsidian 1.0.0 release notes](https://forum.obsidian.md/t/obsidian-release-v1-0-0/44873#tabs-1).
+
+> [!released]
+> Navigating directly to the task line was introduced in Tasks X.Y.Z.
+
+## Limitations
+
+- When in Reading mode and clicking a backlink, and switching to Edit mode to edit the task line, the pane does not scroll to the same position it was in Reading Mode.
+  - We are tracking this in [issue #1879](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1879).
+
+## Support
+
+We use the label `scope: backlinks` to track feedback on backlinks:
+
+- [Open Backlink-related Issues](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22scope%3A+backlinks%22)
+- [Open Backlink-related Discussions](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions?discussions_q=is%3Aopen+label%3A%22scope%3A+backlinks%22+sort%3Atop)
diff --git a/docs/How To/Find tasks with invalid data.md b/docs/How To/Find tasks with invalid data.md
index e742b171..15c05363 100644
--- a/docs/How To/Find tasks with invalid data.md
+++ b/docs/How To/Find tasks with invalid data.md
@@ -69,7 +69,7 @@ group by path
 
 Errors with invalid dates are not easily fixed using the [[Create or edit Task|‘Create or edit Task’ Modal]], as it shows any invalid dates with placeholder text, instead of showing the original values.
 
-So to fix any found tasks, use the backlink to navigate to the original line and fix the error there.
+So to fix any found tasks, use the [[Backlinks|backlink]] to navigate to the original line and fix the error there.
 
 ## Invalid recurrence rules
 
diff --git a/docs/How To/How to style backlinks.md b/docs/How To/How to style backlinks.md
index 51fb9c2b..70ab20a6 100644
--- a/docs/How To/How to style backlinks.md
+++ b/docs/How To/How to style backlinks.md
@@ -4,13 +4,13 @@ publish: true
 
 # How to style backlinks
 
-<span class="related-pages">#css</span>
+<span class="related-pages">#css #feature/backlinks </span>
 
 ## Motivation and assumptions
 
 In Tasks results, by default each task is displayed with its filename,
 and the name of the previous heading, for example `(ACME > Steps to world domination)`.
-This is called a **backlink**.
+This is called a **[[Backlinks|Backlink]]**.
 
 If you don't like how backlinks look, this guide shows how you can modify their appearance.
 
diff --git a/docs/Introduction.md b/docs/Introduction.md
index 5723004f..f0043694 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -6,7 +6,9 @@ publish: true
 
 ## What's New?
 
-- 3.3.0: 🔥 Multiple [[About Task Formats|Task Format]] support - starting with [[Dataview Format]]
+- X.Y.Z: 🔥 Clicking on a [[Backlinks|Backlink]] jumps to the exact task line.
+- X.Y.Z: Tasks now requires at least Obsidian 1.1.1.
+- 3.3.0: 🔥 Multiple [[About Task Formats|Task Format]] support - starting with [[Dataview Format]].
 
 ## Task management for the Obsidian knowledge base
 
diff --git a/docs/Queries/Filters.md b/docs/Queries/Filters.md
index f461f4a5..8fdf8755 100644
--- a/docs/Queries/Filters.md
+++ b/docs/Queries/Filters.md
@@ -155,7 +155,7 @@ Any such mistakes can be found systematically with this search:
     ```
 
 > [!warning]
-> If the above search finds any tasks with invalid dates, they are best fixed by clicking on the backlink to navigate
+> If the above search finds any tasks with invalid dates, they are best fixed by clicking on the [[Backlinks|backlink]] to navigate
 to the incorrect line, and fixing it by directly typing in the new date.
 
 If you use the 'Create or edit Task' Modal, it will discard the broken date, and there will be no information about
@@ -530,6 +530,32 @@ Note that the path includes the `.md` extension.
 > [!released]
 `regex matches` and `regex does not match` were introduced in Tasks 1.12.0.
 
+### Root
+
+> [!released]
+> Introduced in Tasks X.Y.Z.
+
+The `root` is the top-level folder of the file that contains the task, that is, the first directory in the path, which will be `/` for files in root of the vault.
+
+- `root (includes|does not include) <root>`
+  - Matches case-insensitive (disregards capitalization).
+- `root (regex matches|regex does not match) /<JavaScript-style Regex>/`
+  - Does regular expression match (case-sensitive by default).
+  - Essential reading: [[Regular Expressions|Regular Expression Searches]].
+
+### Folder
+
+> [!released]
+> Introduced in Tasks X.Y.Z.
+
+This is the `folder` to the file that contains the task, which will be `/` for files in root of the vault.
+
+- `folder (includes|does not include) <folder>`
+  - Matches case-insensitive (disregards capitalization).
+- `folder (regex matches|regex does not match) /<JavaScript-style Regex>/`
+  - Does regular expression match (case-sensitive by default).
+  - Essential reading: [[Regular Expressions|Regular Expression Searches]].
+
 ### File Name
 
 > [!released]
diff --git a/docs/Queries/Grouping.md b/docs/Queries/Grouping.md
index 92b67d0e..44d4702b 100644
--- a/docs/Queries/Grouping.md
+++ b/docs/Queries/Grouping.md
@@ -30,7 +30,7 @@ You can group by the following properties.
 
 ### File contents
 
-1. `backlink` (the text that would be shown in the task's backlink, combining the task's file name and heading, but with no link added)
+1. `backlink` (the text that would be shown in the task's [[Backlinks|backlink]], combining the task's file name and heading, but with no link added)
 1. `heading` (the heading preceding the task, or `(No heading)` if there are no headings in the file)
 
 ### Task date properties
diff --git a/docs/Quick Reference/Quick Reference.md b/docs/Quick Reference/Quick Reference.md
index eb23d150..3766e0aa 100644
--- a/docs/Quick Reference/Quick Reference.md
+++ b/docs/Quick Reference/Quick Reference.md
@@ -22,8 +22,8 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `priority is (above, below, not)? (low, none, medium, high)`                                                                                                                                                                                                                                    | `sort by priority`                          | `group by priority`    | `hide priority`        |
 |                                                                                                                                                                                                                                                                                                 | `sort by urgency`                           |                        | `show urgency`         |
 | `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`      |                        |
+| `root (includes, does not include) <root>`<br>`root (regex matches, regex does not match) /regex/i`                                                                                                                                                                                             |                                             | `group by root`        |                        |
+| `folder (includes, does not include) <folder>`<br>`folder (regex matches, regex does not match) /regex/i`                                                                                                                                                                                             |                                             | `group by folder`      |                        |
 | `filename (includes, does not include) <filename>`<br>`filename (regex matches, regex does not match) /regex/i`                                                                                                                                                                                 | `sort by filename`                          | `group by filename`    |                        |
 | `heading (includes, does not include) <string>`<br>`heading (regex matches, regex does not match) /regex/i`                                                                                                                                                                                     | `sort by heading`                           | `group by heading`     |                        |
 |                                                                                                                                                                                                                                                                                                 |                                             | `group by backlink`    | `hide backlink`        |
diff --git a/manifest.json b/manifest.json
index 8ad7b69f..f9c52a99 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,10 +2,10 @@
   "id": "obsidian-tasks-plugin",
   "name": "Tasks",
   "version": "3.3.0",
-  "minAppVersion": "0.14.6",
+  "minAppVersion": "1.1.1",
   "description": "Task management for Obsidian",
   "author": "Martin Schenck and Clare Macrae",
   "authorUrl": "https://github.com/obsidian-tasks-group",
   "fundingUrl": "https://github.com/sponsors/claremacrae",
   "isDesktopOnly": false
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index fb19ed1b..1b9e913d 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
     "@testing-library/svelte": "^3.2.2",
     "@tsconfig/svelte": "^3.0.0",
     "@types/jest": "^29.2.3",
-    "@typescript-eslint/eslint-plugin": "^5.36.1",
+    "@typescript-eslint/eslint-plugin": "^5.59.0",
     "@typescript-eslint/parser": "^5.36.1",
     "approvals": "^6.2.1",
     "async-mutex": "^0.4.0",
@@ -37,14 +37,14 @@
     "esbuild": "0.15.6",
     "esbuild-svelte": "^0.7.1",
     "eslint": "^8.23.0",
-    "eslint-config-prettier": "^8.5.0",
+    "eslint-config-prettier": "^8.8.0",
     "eslint-plugin-import": "^2.22.1",
     "eslint-plugin-prettier": "^4.2.1",
     "jest": "^29.3.1",
     "jest-environment-jsdom": "^29.3.1",
     "markdownlint-cli2": "^0.5.1",
     "moment": "^2.29.4",
-    "obsidian": "0.14.6",
+    "obsidian": "^1.1.1",
     "prettier": "^2.2.1",
     "prettier-plugin-svelte": "^2.10.0",
     "svelte": "^3.48.0",
diff --git a/resources/sample_vaults/Tasks-Demo/.obsidian/community-plugins.json b/resources/sample_vaults/Tasks-Demo/.obsidian/community-plugins.json
index ae67773a..77b09a5a 100644
--- a/resources/sample_vaults/Tasks-Demo/.obsidian/community-plugins.json
+++ b/resources/sample_vaults/Tasks-Demo/.obsidian/community-plugins.json
@@ -2,5 +2,6 @@
   "obsidian-tasks-plugin",
   "dataview",
   "obsidian-style-settings",
-  "cmdr"
+  "cmdr",
+  "vault-name-status-bar"
 ]
\ No newline at end of file
diff --git a/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/main.js b/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/main.js
new file mode 100644
index 00000000..05bd9cf0
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/main.js
@@ -0,0 +1,86 @@
+/*
+THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
+if you want to view the source, please visit the github repository of this plugin
+*/
+
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __export = (target, all) => {
+  for (var name in all)
+    __defProp(target, name, { get: all[name], enumerable: true });
+};
+var __copyProps = (to, from, except, desc) => {
+  if (from && typeof from === "object" || typeof from === "function") {
+    for (let key of __getOwnPropNames(from))
+      if (!__hasOwnProp.call(to, key) && key !== except)
+        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+  }
+  return to;
+};
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+
+// src/main.ts
+var main_exports = {};
+__export(main_exports, {
+  default: () => StatusBarVaultName
+});
+module.exports = __toCommonJS(main_exports);
+var import_obsidian = require("obsidian");
+var DEFAULT_SETTINGS = {
+  reducedAtStart: false
+};
+var StatusBarVaultName = class extends import_obsidian.Plugin {
+  async onload() {
+    await this.loadSettings();
+    this.addSettingTab(new Settings(this.app, this));
+    const { workspace } = this.app;
+    const vaultName = this.app.vault.getName();
+    const statusBar = document.querySelector(".status-bar");
+    this.title = document.createElement("div");
+    this.settings.reducedAtStart ? this.title.innerText = "::" : this.title.innerText = vaultName;
+    this.title.classList.add("status-bar-vault-name");
+    statusBar == null ? void 0 : statusBar.prepend(this.title);
+    this.title.addEventListener("click", () => {
+      if (this.title.innerText === vaultName) {
+        this.title.innerText = "::";
+      } else {
+        this.title.innerText = vaultName;
+      }
+    });
+  }
+  onunload() {
+    this.title.detach();
+  }
+  async loadSettings() {
+    this.settings = Object.assign(
+      {},
+      DEFAULT_SETTINGS,
+      await this.loadData()
+    );
+  }
+  async saveSettings() {
+    await this.saveData(this.settings);
+  }
+};
+var Settings = class extends import_obsidian.PluginSettingTab {
+  constructor(app, plugin) {
+    super(app, plugin);
+    this.plugin = plugin;
+  }
+  display() {
+    const { containerEl } = this;
+    containerEl.empty();
+    containerEl.createEl("h2", { text: "Vaut Name in Status Bar" });
+    containerEl.createEl("p", { text: "Click on the vault name or on :: to reduce or enlarge" });
+    new import_obsidian.Setting(containerEl).setName("Reduced at Start").setDesc("Will be reduced by default").addToggle((toggle) => {
+      toggle.setValue(this.plugin.settings.reducedAtStart).onChange((value) => {
+        this.plugin.settings.reducedAtStart = value;
+        this.plugin.saveSettings();
+        this.plugin.settings.reducedAtStart ? this.plugin.title.innerText = "::" : this.plugin.title.innerText = this.app.vault.getName();
+      });
+    });
+  }
+};
+//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL21haW4udHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImltcG9ydCB7IEFwcCwgUGx1Z2luLCBQbHVnaW5TZXR0aW5nVGFiLCBTZXR0aW5nLCBWaWV3IH0gZnJvbSBcIm9ic2lkaWFuXCI7XHJcblxyXG5pbnRlcmZhY2UgU0JWTlNldHRpbmdzIHtcclxuXHRyZWR1Y2VkQXRTdGFydDogYm9vbGVhbjtcclxufVxyXG5cclxuY29uc3QgREVGQVVMVF9TRVRUSU5HUzogU0JWTlNldHRpbmdzID0ge1xyXG5cdHJlZHVjZWRBdFN0YXJ0OiBmYWxzZSxcclxufTtcclxuXHJcbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFN0YXR1c0JhclZhdWx0TmFtZSBleHRlbmRzIFBsdWdpbiB7XHJcblx0c2V0dGluZ3M6IFNCVk5TZXR0aW5nc1xyXG5cdHRpdGxlOiBIVE1MRGl2RWxlbWVudFxyXG5cclxuXHRhc3luYyBvbmxvYWQoKSB7XHJcblx0XHRhd2FpdCB0aGlzLmxvYWRTZXR0aW5ncygpO1xyXG5cdFx0dGhpcy5hZGRTZXR0aW5nVGFiKG5ldyBTZXR0aW5ncyh0aGlzLmFwcCwgdGhpcykpO1xyXG5cdFx0Y29uc3QgeyB3b3Jrc3BhY2UgfSA9IHRoaXMuYXBwO1xyXG5cdFx0Y29uc3QgdmF1bHROYW1lID0gdGhpcy5hcHAudmF1bHQuZ2V0TmFtZSgpO1xyXG5cdFx0Y29uc3Qgc3RhdHVzQmFyID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcignLnN0YXR1cy1iYXInKVxyXG5cdFx0dGhpcy50aXRsZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpO1xyXG5cdFx0dGhpcy5zZXR0aW5ncy5yZWR1Y2VkQXRTdGFydCA/IHRoaXMudGl0bGUuaW5uZXJUZXh0ID0gXCI6OlwiIDogdGhpcy50aXRsZS5pbm5lclRleHQgPSB2YXVsdE5hbWVcclxuXHRcdHRoaXMudGl0bGUuY2xhc3NMaXN0LmFkZChcInN0YXR1cy1iYXItdmF1bHQtbmFtZVwiKTtcclxuXHRcdHN0YXR1c0Jhcj8ucHJlcGVuZCh0aGlzLnRpdGxlKVxyXG5cclxuXHRcdHRoaXMudGl0bGUuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcblx0XHRcdGlmICh0aGlzLnRpdGxlLmlubmVyVGV4dCA9PT0gdmF1bHROYW1lKSB7XHJcblx0XHRcdFx0dGhpcy50aXRsZS5pbm5lclRleHQgPSBcIjo6XCI7XHJcblx0XHRcdH0gZWxzZSB7XHJcblx0XHRcdFx0dGhpcy50aXRsZS5pbm5lclRleHQgPSB2YXVsdE5hbWU7XHJcblx0XHRcdH1cclxuXHRcdH0pO1xyXG5cclxuXHR9XHJcblxyXG5cdG9udW5sb2FkKCkge1xyXG5cdFx0dGhpcy50aXRsZS5kZXRhY2goKVxyXG5cclxuXHR9XHJcblx0YXN5bmMgbG9hZFNldHRpbmdzKCkge1xyXG5cdFx0dGhpcy5zZXR0aW5ncyA9IE9iamVjdC5hc3NpZ24oXHJcblx0XHRcdHt9LFxyXG5cdFx0XHRERUZBVUxUX1NFVFRJTkdTLFxyXG5cdFx0XHRhd2FpdCB0aGlzLmxvYWREYXRhKClcclxuXHRcdCk7XHJcblx0fVxyXG5cclxuXHRhc3luYyBzYXZlU2V0dGluZ3MoKSB7XHJcblx0XHRhd2FpdCB0aGlzLnNhdmVEYXRhKHRoaXMuc2V0dGluZ3MpO1xyXG5cdH1cclxuXHJcbn1cclxuXHJcbmNsYXNzIFNldHRpbmdzIGV4dGVuZHMgUGx1Z2luU2V0dGluZ1RhYiB7XHJcblx0cGx1Z2luOiBTdGF0dXNCYXJWYXVsdE5hbWU7XHJcblxyXG5cdGNvbnN0cnVjdG9yKGFwcDogQXBwLCBwbHVnaW46IFN0YXR1c0JhclZhdWx0TmFtZSkge1xyXG5cdFx0c3VwZXIoYXBwLCBwbHVnaW4pO1xyXG5cdFx0dGhpcy5wbHVnaW4gPSBwbHVnaW47XHJcblx0fVxyXG5cclxuXHRkaXNwbGF5KCk6IHZvaWQge1xyXG5cdFx0Y29uc3QgeyBjb250YWluZXJFbCB9ID0gdGhpcztcclxuXHJcblx0XHRjb250YWluZXJFbC5lbXB0eSgpO1xyXG5cclxuXHRcdGNvbnRhaW5lckVsLmNyZWF0ZUVsKCdoMicsIHsgdGV4dDogJ1ZhdXQgTmFtZSBpbiBTdGF0dXMgQmFyJyB9KTtcclxuXHRcdGNvbnRhaW5lckVsLmNyZWF0ZUVsKCdwJywgeyB0ZXh0OiAnQ2xpY2sgb24gdGhlIHZhdWx0IG5hbWUgb3Igb24gOjogdG8gcmVkdWNlIG9yIGVubGFyZ2UnIH0pO1xyXG5cclxuXHRcdG5ldyBTZXR0aW5nKGNvbnRhaW5lckVsKVxyXG5cdFx0XHQuc2V0TmFtZShcIlJlZHVjZWQgYXQgU3RhcnRcIilcclxuXHRcdFx0LnNldERlc2MoXCJXaWxsIGJlIHJlZHVjZWQgYnkgZGVmYXVsdFwiKVxyXG5cdFx0XHQuYWRkVG9nZ2xlKCh0b2dnbGUpID0+IHtcclxuXHRcdFx0XHR0b2dnbGVcclxuXHRcdFx0XHRcdC5zZXRWYWx1ZSh0aGlzLnBsdWdpbi5zZXR0aW5ncy5yZWR1Y2VkQXRTdGFydClcclxuXHRcdFx0XHRcdC5vbkNoYW5nZSgodmFsdWUpID0+IHtcclxuXHRcdFx0XHRcdFx0dGhpcy5wbHVnaW4uc2V0dGluZ3MucmVkdWNlZEF0U3RhcnQgPSB2YWx1ZTtcclxuXHRcdFx0XHRcdFx0dGhpcy5wbHVnaW4uc2F2ZVNldHRpbmdzKCk7XHJcblx0XHRcdFx0XHRcdHRoaXMucGx1Z2luLnNldHRpbmdzLnJlZHVjZWRBdFN0YXJ0XHJcblx0XHRcdFx0XHRcdFx0PyAodGhpcy5wbHVnaW4udGl0bGUuaW5uZXJUZXh0ID0gXCI6OlwiKVxyXG5cdFx0XHRcdFx0XHRcdDogKHRoaXMucGx1Z2luLnRpdGxlLmlubmVyVGV4dCA9IHRoaXMuYXBwLnZhdWx0LmdldE5hbWUoKSk7XHJcblx0XHRcdFx0XHR9KTtcclxuXHRcdFx0fSk7XHJcblx0fVxyXG59XHJcblxyXG4iXSwKICAibWFwcGluZ3MiOiAiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUEsc0JBQTZEO0FBTTdELElBQU0sbUJBQWlDO0FBQUEsRUFDdEMsZ0JBQWdCO0FBQ2pCO0FBRUEsSUFBcUIscUJBQXJCLGNBQWdELHVCQUFPO0FBQUEsRUFJdEQsTUFBTSxTQUFTO0FBQ2QsVUFBTSxLQUFLLGFBQWE7QUFDeEIsU0FBSyxjQUFjLElBQUksU0FBUyxLQUFLLEtBQUssSUFBSSxDQUFDO0FBQy9DLFVBQU0sRUFBRSxVQUFVLElBQUksS0FBSztBQUMzQixVQUFNLFlBQVksS0FBSyxJQUFJLE1BQU0sUUFBUTtBQUN6QyxVQUFNLFlBQVksU0FBUyxjQUFjLGFBQWE7QUFDdEQsU0FBSyxRQUFRLFNBQVMsY0FBYyxLQUFLO0FBQ3pDLFNBQUssU0FBUyxpQkFBaUIsS0FBSyxNQUFNLFlBQVksT0FBTyxLQUFLLE1BQU0sWUFBWTtBQUNwRixTQUFLLE1BQU0sVUFBVSxJQUFJLHVCQUF1QjtBQUNoRCwyQ0FBVyxRQUFRLEtBQUs7QUFFeEIsU0FBSyxNQUFNLGlCQUFpQixTQUFTLE1BQU07QUFDMUMsVUFBSSxLQUFLLE1BQU0sY0FBYyxXQUFXO0FBQ3ZDLGFBQUssTUFBTSxZQUFZO0FBQUEsTUFDeEIsT0FBTztBQUNOLGFBQUssTUFBTSxZQUFZO0FBQUEsTUFDeEI7QUFBQSxJQUNELENBQUM7QUFBQSxFQUVGO0FBQUEsRUFFQSxXQUFXO0FBQ1YsU0FBSyxNQUFNLE9BQU87QUFBQSxFQUVuQjtBQUFBLEVBQ0EsTUFBTSxlQUFlO0FBQ3BCLFNBQUssV0FBVyxPQUFPO0FBQUEsTUFDdEIsQ0FBQztBQUFBLE1BQ0Q7QUFBQSxNQUNBLE1BQU0sS0FBSyxTQUFTO0FBQUEsSUFDckI7QUFBQSxFQUNEO0FBQUEsRUFFQSxNQUFNLGVBQWU7QUFDcEIsVUFBTSxLQUFLLFNBQVMsS0FBSyxRQUFRO0FBQUEsRUFDbEM7QUFFRDtBQUVBLElBQU0sV0FBTixjQUF1QixpQ0FBaUI7QUFBQSxFQUd2QyxZQUFZLEtBQVUsUUFBNEI7QUFDakQsVUFBTSxLQUFLLE1BQU07QUFDakIsU0FBSyxTQUFTO0FBQUEsRUFDZjtBQUFBLEVBRUEsVUFBZ0I7QUFDZixVQUFNLEVBQUUsWUFBWSxJQUFJO0FBRXhCLGdCQUFZLE1BQU07QUFFbEIsZ0JBQVksU0FBUyxNQUFNLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUM5RCxnQkFBWSxTQUFTLEtBQUssRUFBRSxNQUFNLHdEQUF3RCxDQUFDO0FBRTNGLFFBQUksd0JBQVEsV0FBVyxFQUNyQixRQUFRLGtCQUFrQixFQUMxQixRQUFRLDRCQUE0QixFQUNwQyxVQUFVLENBQUMsV0FBVztBQUN0QixhQUNFLFNBQVMsS0FBSyxPQUFPLFNBQVMsY0FBYyxFQUM1QyxTQUFTLENBQUMsVUFBVTtBQUNwQixhQUFLLE9BQU8sU0FBUyxpQkFBaUI7QUFDdEMsYUFBSyxPQUFPLGFBQWE7QUFDekIsYUFBSyxPQUFPLFNBQVMsaUJBQ2pCLEtBQUssT0FBTyxNQUFNLFlBQVksT0FDOUIsS0FBSyxPQUFPLE1BQU0sWUFBWSxLQUFLLElBQUksTUFBTSxRQUFRO0FBQUEsTUFDMUQsQ0FBQztBQUFBLElBQ0gsQ0FBQztBQUFBLEVBQ0g7QUFDRDsiLAogICJuYW1lcyI6IFtdCn0K
diff --git a/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/manifest.json b/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/manifest.json
new file mode 100644
index 00000000..2ee861a8
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/manifest.json
@@ -0,0 +1,10 @@
+{
+	"id": "vault-name-status-bar",
+	"name": "Vault name in status bar",
+	"version": "1.0.1",
+	"minAppVersion": "1.1.0",
+	"description": "Vault name in status bar. can be reduced to ::",
+	"author": "1C0D",
+	"authorUrl": "",
+	"isDesktopOnly": true
+}
diff --git a/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/styles.css b/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/styles.css
new file mode 100644
index 00000000..8a56f1a7
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/.obsidian/plugins/vault-name-status-bar/styles.css
@@ -0,0 +1,5 @@
+.status-bar-vault-name {
+  font-size: 110%;
+  font-weight: bold;
+  color: rgb(0, 115, 255);
+}
\ No newline at end of file
diff --git a/src/Commands/ToggleDone.ts b/src/Commands/ToggleDone.ts
index 9644179b..921565aa 100644
--- a/src/Commands/ToggleDone.ts
+++ b/src/Commands/ToggleDone.ts
@@ -1,10 +1,10 @@
-import { Editor, type EditorPosition, MarkdownView, View } from 'obsidian';
+import { Editor, type EditorPosition, type MarkdownFileInfo, MarkdownView } from 'obsidian';
 import { StatusRegistry } from '../StatusRegistry';
 
 import { Task, TaskRegularExpressions } from '../Task';
 import { TaskLocation } from '../TaskLocation';
 
-export const toggleDone = (checking: boolean, editor: Editor, view: View) => {
+export const toggleDone = (checking: boolean, editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
     if (checking) {
         if (!(view instanceof MarkdownView)) {
             // If we are not in a markdown view, the command shouldn't be shown.
diff --git a/src/Commands/index.ts b/src/Commands/index.ts
index b235ba11..0ec155ad 100644
--- a/src/Commands/index.ts
+++ b/src/Commands/index.ts
@@ -1,4 +1,4 @@
-import type { App, Editor, Plugin, View } from 'obsidian';
+import type { App, Editor, MarkdownFileInfo, MarkdownView, Plugin, View } from 'obsidian';
 import { createOrEdit } from './CreateOrEdit';
 
 import { toggleDone } from './ToggleDone';
@@ -17,8 +17,8 @@ export class Commands {
             id: 'edit-task',
             name: 'Create or edit task',
             icon: 'pencil',
-            editorCheckCallback: (checking: boolean, editor: Editor, view: View) => {
-                return createOrEdit(checking, editor, view, this.app);
+            editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
+                return createOrEdit(checking, editor, view as View, this.app);
             },
         });
 
diff --git a/src/File.ts b/src/File.ts
index ca079309..56c301bc 100644
--- a/src/File.ts
+++ b/src/File.ts
@@ -81,6 +81,12 @@ function debugLog(message: string) {
     logger.debug(message);
 }
 
+// When this exception is thrown, it is meant to indicate that the caller should consider to try the operation
+// again soon
+class WarningWorthRetrying extends Error {}
+// Same as above, but be silent about it
+class RetryWithoutWarning extends Error {}
+
 /**
  * This is a workaround to re-try when the returned file cache is `undefined`.
  * Retrying after a while may return a valid file cache.
@@ -138,37 +144,63 @@ Recommendations:
         }, timeout);
     };
 
+    try {
+        const [taskLineNumber, file, fileLines] = await getTaskAndFileLines(originalTask, vault);
+        // Finally, we can insert 1 or more lines over the original task line:
+        const updatedFileLines = [
+            ...fileLines.slice(0, taskLineNumber),
+            ...newTasks.map((task: Task) => task.toFileLineString()),
+            ...fileLines.slice(taskLineNumber + 1), // Only supports single-line tasks.
+        ];
+
+        await vault.modify(file, updatedFileLines.join('\n'));
+    } catch (e) {
+        if (e instanceof WarningWorthRetrying) {
+            if (e.message) warnAndNotice(e.message);
+            return retry();
+        } else if (e instanceof RetryWithoutWarning) {
+            return retry();
+        } else if (e instanceof Error) {
+            errorAndNotice(e.message);
+        }
+    }
+};
+
+/*
+ * This method returns the line on which `task` is defined, together with the file it is defined in, and the
+ * lines of that file, possibly for the purpose of updating the task in the file or jumping to it.
+ * It may throw a WarningWorthRetrying exception in several cases that justify a retry (should be handled by the caller)
+ * or an Error exception in case of an unrecoverable error.
+ */
+async function getTaskAndFileLines(task: Task, vault: Vault): Promise<[number, TFile, string[]]> {
+    if (metadataCache === undefined) throw new WarningWorthRetrying();
     // Validate our inputs.
     // For permanent failures, return nothing.
     // For failures that might be fixed if we wait for a little while, return retry().
-    const file = vault.getAbstractFileByPath(originalTask.path);
+    const file = vault.getAbstractFileByPath(task.path);
     if (!(file instanceof TFile)) {
-        warnAndNotice(`Tasks: No file found for task ${originalTask.description}. Retrying ...`);
-        return retry();
+        throw new WarningWorthRetrying(`Tasks: No file found for task ${task.description}. Retrying ...`);
     }
 
     if (!supportedFileExtensions.includes(file.extension)) {
-        errorAndNotice(`Tasks: Does not support files with the ${file.extension} file extension.`);
-        return;
+        throw new Error(`Tasks: Does not support files with the ${file.extension} file extension.`);
     }
 
     const fileCache = metadataCache.getFileCache(file);
     if (fileCache == undefined || fileCache === null) {
-        warnAndNotice(`Tasks: No file cache found for file ${file.path}. Retrying ...`);
-        return retry();
+        throw new WarningWorthRetrying(`Tasks: No file cache found for file ${file.path}. Retrying ...`);
     }
 
     const listItemsCache = fileCache.listItems;
     if (listItemsCache === undefined || listItemsCache.length === 0) {
-        warnAndNotice(`Tasks: No list items found in file cache of ${file.path}. Retrying ...`);
-        return retry();
+        throw new WarningWorthRetrying(`Tasks: No list items found in file cache of ${file.path}. Retrying ...`);
     }
 
     // We can now try and find which line in the file currently contains originalTask,
     // so that we know which line to update.
     const fileContent = await vault.read(file); // TODO: replace with vault.process.
     const fileLines = fileContent.split('\n');
-    const taskLineNumber = findLineNumberOfTaskToToggle(originalTask, fileLines, listItemsCache, debugLog);
+    const taskLineNumber = findLineNumberOfTaskToToggle(task, fileLines, listItemsCache, debugLog);
 
     if (taskLineNumber === undefined) {
         const logDataForMocking = false;
@@ -177,20 +209,28 @@ Recommendations:
             // so write out to the console a representation of the data needed to reconstruct the above
             // findLineNumberOfTaskToToggle() call, so that the content can be saved
             // to a JSON file and then re-used in a 'unit' test.
-            saveMockDataForTesting(originalTask, fileLines, listItemsCache);
+            saveMockDataForTesting(task, fileLines, listItemsCache);
         }
-        return retry();
+        throw new RetryWithoutWarning();
     }
+    return [taskLineNumber, file, fileLines];
+}
 
-    // Finally, we can insert 1 or more lines over the original task line:
-    const updatedFileLines = [
-        ...fileLines.slice(0, taskLineNumber),
-        ...newTasks.map((task: Task) => task.toFileLineString()),
-        ...fileLines.slice(taskLineNumber + 1), // Only supports single-line tasks.
-    ];
-
-    await vault.modify(file, updatedFileLines.join('\n'));
-};
+// A simpler version of the method above, which doesn't return the lines of the file, and handles exceptions
+// internally rather than throw them to the outside
+export async function getTaskLineAndFile(task: Task, vault: Vault): Promise<[number, TFile] | undefined> {
+    try {
+        const [taskLineNumber, file, _] = await getTaskAndFileLines(task, vault);
+        return [taskLineNumber, file];
+    } catch (e) {
+        if (e instanceof WarningWorthRetrying) {
+            if (e.message) warnAndNotice(e.message);
+        } else if (e instanceof Error) {
+            errorAndNotice(e.message);
+        }
+    }
+    return undefined;
+}
 
 function isValidLineNumber(listItemLineNumber: number, fileLines: string[]) {
     return listItemLineNumber < fileLines.length;
diff --git a/src/Query/Filter/BacklinkField.ts b/src/Query/Filter/BacklinkField.ts
new file mode 100644
index 00000000..1b0c0b5b
--- /dev/null
+++ b/src/Query/Filter/BacklinkField.ts
@@ -0,0 +1,66 @@
+import { Group } from '../Group';
+import type { Task } from '../../Task';
+import type { GrouperFunction } from '../Grouper';
+import { TextField } from './TextField';
+import { HeadingField } from './HeadingField';
+import { FilterOrErrorMessage } from './Filter';
+
+export class BacklinkField extends TextField {
+    public fieldName(): string {
+        return 'backlink';
+    }
+
+    public value(task: Task): string {
+        const linkText = task.getLinkText({ isFilenameUnique: true });
+        if (linkText === null) {
+            return 'Unknown Location';
+        }
+
+        let filenameComponent = 'Unknown Location';
+
+        if (task.filename !== null) {
+            filenameComponent = task.filename;
+        }
+
+        if (task.precedingHeader === null || task.precedingHeader.length === 0) {
+            return filenameComponent;
+        }
+
+        // Markdown characters in the heading must NOT be escaped.
+        const headingGrouper = new HeadingField().createGrouper().grouper;
+        const headingComponent = headingGrouper(task)[0];
+
+        if (filenameComponent === headingComponent) {
+            return filenameComponent;
+        } else {
+            return `${filenameComponent} > ${headingComponent}`;
+        }
+    }
+
+    createFilterOrErrorMessage(line: string): FilterOrErrorMessage {
+        return FilterOrErrorMessage.fromError(line, 'backlink field does not support filtering');
+    }
+
+    canCreateFilterForLine(_line: string): boolean {
+        return false;
+    }
+
+    public supportsGrouping(): boolean {
+        return true;
+    }
+
+    public grouper(): GrouperFunction {
+        return (task: Task) => {
+            const filename = task.filename;
+            if (filename !== null) {
+                const backlink = this.value(task);
+                if (filename !== backlink) {
+                    // In case backlink is 'file_name > heading', the filename only shall be escaped
+                    return [Group.escapeMarkdownCharacters(filename) + backlink.substring(filename.length)];
+                }
+            }
+
+            return [Group.escapeMarkdownCharacters(this.value(task))];
+        };
+    }
+}
diff --git a/src/Query/Filter/FolderField.ts b/src/Query/Filter/FolderField.ts
new file mode 100644
index 00000000..c74f6d6d
--- /dev/null
+++ b/src/Query/Filter/FolderField.ts
@@ -0,0 +1,30 @@
+import { Group } from '../Group';
+import type { Task } from '../../Task';
+import type { GrouperFunction } from '../Grouper';
+import { TextField } from './TextField';
+
+export class FolderField extends TextField {
+    public fieldName(): string {
+        return 'folder';
+    }
+
+    public value(task: Task): string {
+        const path = task.path;
+        const fileNameWithExtension = task.filename + '.md';
+        const folder = path.substring(0, path.lastIndexOf(fileNameWithExtension));
+        if (folder === '') {
+            return '/';
+        }
+        return folder;
+    }
+
+    public supportsGrouping(): boolean {
+        return true;
+    }
+
+    public grouper(): GrouperFunction {
+        return (task: Task) => {
+            return [Group.escapeMarkdownCharacters(this.value(task))];
+        };
+    }
+}
diff --git a/src/Query/Filter/RootField.ts b/src/Query/Filter/RootField.ts
new file mode 100644
index 00000000..1b475047
--- /dev/null
+++ b/src/Query/Filter/RootField.ts
@@ -0,0 +1,34 @@
+import { Group } from '../Group';
+import type { Task } from '../../Task';
+import type { GrouperFunction } from '../Grouper';
+import { TextField } from './TextField';
+
+export class RootField extends TextField {
+    public fieldName(): string {
+        return 'root';
+    }
+
+    public value(task: Task): string {
+        let path = task.path.replace(/\\/g, '/');
+
+        if (path.charAt(0) === '/') {
+            path = path.substring(1);
+        }
+
+        const separatorIndex = path.indexOf('/');
+        if (separatorIndex == -1) {
+            return '/';
+        }
+        return path.substring(0, separatorIndex + 1);
+    }
+
+    public supportsGrouping(): boolean {
+        return true;
+    }
+
+    public grouper(): GrouperFunction {
+        return (task: Task) => {
+            return [Group.escapeMarkdownCharacters(this.value(task))];
+        };
+    }
+}
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index 21839504..d7bd8b9f 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -23,6 +23,9 @@ import type { FilterOrErrorMessage } from './Filter/Filter';
 import type { Sorter } from './Sorter';
 import type { Grouper } from './Grouper';
 import { MultiTextField } from './Filter/MultiTextField';
+import { FolderField } from './Filter/FolderField';
+import { RootField } from './Filter/RootField';
+import { BacklinkField } from './Filter/BacklinkField';
 
 const fieldCreators = [
     () => new StatusNameField(), // status.name is before status, to avoid ambiguity
@@ -37,6 +40,9 @@ const fieldCreators = [
     () => new DueDateField(),
     () => new DoneDateField(),
     () => new PathField(),
+    () => new FolderField(),
+    () => new RootField(),
+    () => new BacklinkField(),
     () => new DescriptionField(),
     () => new TagsField(),
     () => new HeadingField(),
diff --git a/src/Query/Group.ts b/src/Query/Group.ts
index 9f869c3d..289efcb0 100644
--- a/src/Query/Group.ts
+++ b/src/Query/Group.ts
@@ -1,17 +1,11 @@
 import type { Task } from '../Task';
 import { TaskGroups } from './TaskGroups';
-import { Grouper } from './Grouper';
-import type { GrouperFunction, GroupingProperty } from './Grouper';
-import { HeadingField } from './Filter/HeadingField';
+import type { Grouper } from './Grouper';
 
 /**
  * Implementation of the 'group by' instruction.
  */
 export class Group {
-    public static fromGroupingProperty(property: GroupingProperty): Grouper {
-        return new Grouper(property, Group.grouperForProperty(property));
-    }
-
     /**
      * Group a list of tasks, according to one or more task properties
      * @param grouping 0 or more Grouping values, one per 'group by' line
@@ -33,65 +27,8 @@ export class Group {
         return grouping.grouper(task);
     }
 
-    public static grouperForProperty(property: GroupingProperty): GrouperFunction {
-        return Group.groupers[property];
-    }
-
-    private static groupers: Record<GroupingProperty, GrouperFunction> = {
-        backlink: Group.groupByBacklink,
-        folder: Group.groupByFolder,
-        root: Group.groupByRoot,
-    };
-
     public static escapeMarkdownCharacters(filename: string) {
         // https://wilsonmar.github.io/markdown-text-for-github-from-html/#special-characters
         return filename.replace(/\\/g, '\\\\').replace(/_/g, '\\_');
     }
-
-    private static groupByFolder(task: Task): string[] {
-        const path = task.path;
-        const fileNameWithExtension = task.filename + '.md';
-        const folder = path.substring(0, path.lastIndexOf(fileNameWithExtension));
-        if (folder === '') {
-            return ['/'];
-        }
-        return [Group.escapeMarkdownCharacters(folder)];
-    }
-
-    private static groupByRoot(task: Task): string[] {
-        const path = task.path.replace(/\\/g, '/');
-        const separatorIndex = path.indexOf('/');
-        if (separatorIndex == -1) {
-            return ['/'];
-        }
-        return [Group.escapeMarkdownCharacters(path.substring(0, separatorIndex + 1))];
-    }
-
-    private static groupByBacklink(task: Task): string[] {
-        const linkText = task.getLinkText({ isFilenameUnique: true });
-        if (linkText === null) {
-            return ['Unknown Location'];
-        }
-
-        let filenameComponent = 'Unknown Location';
-
-        if (task.filename !== null) {
-            // Markdown characters in the file name must be escaped.
-            filenameComponent = Group.escapeMarkdownCharacters(task.filename);
-        }
-
-        if (task.precedingHeader === null || task.precedingHeader.length === 0) {
-            return [filenameComponent];
-        }
-
-        // Markdown characters in the heading must NOT be escaped.
-        const headingGrouper = new HeadingField().createGrouper().grouper;
-        const headingComponent = headingGrouper(task)[0];
-
-        if (filenameComponent === headingComponent) {
-            return [filenameComponent];
-        } else {
-            return [`${filenameComponent} > ${headingComponent}`];
-        }
-    }
 }
diff --git a/src/Query/Grouper.ts b/src/Query/Grouper.ts
index 53b52044..89eb69ef 100644
--- a/src/Query/Grouper.ts
+++ b/src/Query/Grouper.ts
@@ -5,8 +5,6 @@ import type { Task } from '../Task';
  */
 export type GrouperFunction = (task: Task) => string[];
 
-export type GroupingProperty = 'backlink' | 'folder' | 'root';
-
 export class Grouper {
     public readonly property: string;
     public readonly grouper: GrouperFunction;
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index 3a9d192d..3bb3b1cc 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -9,7 +9,6 @@ import type { TaskGroups } from './TaskGroups';
 import * as FilterParser from './FilterParser';
 import { Group } from './Group';
 import type { Grouper } from './Grouper';
-import type { GroupingProperty } from './Grouper';
 import type { Filter } from './Filter/Filter';
 
 export class Query implements IQuery {
@@ -22,8 +21,6 @@ export class Query implements IQuery {
     private _sorting: Sorter[] = [];
     private _grouping: Grouper[] = [];
 
-    private readonly groupByRegexp = /^group by (backlink|folder|root)/;
-
     private readonly hideOptionsRegexp =
         /^(hide|show) (task count|backlink|priority|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|urgency)/;
     private readonly shortModeRegexp = /^short/;
@@ -53,10 +50,7 @@ export class Query implements IQuery {
                         break;
                     case this.parseSortBy({ line }):
                         break;
-                    case this.parseGroupBy2({ line }):
-                        break;
-                    case this.groupByRegexp.test(line):
-                        this.parseGroupBy({ line });
+                    case this.parseGroupBy({ line }):
                         break;
                     case this.hideOptionsRegexp.test(line):
                         this.parseHideOptions({ line });
@@ -221,35 +215,13 @@ export class Query implements IQuery {
     }
 
     /**
-     * Old-style parsing of `group by` lines, for grouping that is implemented with static
-     * methods in {@link Group}, that are looked up from a {@link GroupingProperty}.
-     *
-     * These will be gradually migrated to the grouping method in {@link Field}
-     * classes, after which this method will be deleted.
-     *
-     * @param line
-     * @private
-     * @see parseGroupBy2
-     */
-    private parseGroupBy({ line }: { line: string }): void {
-        const fieldMatch = line.match(this.groupByRegexp);
-        if (fieldMatch !== null) {
-            this._grouping.push(Group.fromGroupingProperty(fieldMatch[1] as GroupingProperty));
-        } else {
-            this._error = 'do not understand query grouping';
-        }
-    }
-
-    /**
-     * New-style parsing of `group by` lines, for grouping that is implemented in the {@link Field}
+     * Parsing of `group by` lines, for grouping that is implemented in the {@link Field}
      * classes.
      *
-     * Once the original {@link parseGroupBy} has been removed, rename this to parseGroupBy()
      * @param line
      * @private
-     * @see parseGroupBy
      */
-    private parseGroupBy2({ line }: { line: string }): boolean {
+    private parseGroupBy({ line }: { line: string }): boolean {
         const groupingMaybe = FilterParser.parseGrouper(line);
         if (groupingMaybe) {
             this._grouping.push(groupingMaybe);
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index 8c2e5838..bcac56cb 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -1,9 +1,9 @@
-import { App, MarkdownRenderChild, MarkdownRenderer, Plugin, TFile } from 'obsidian';
+import { App, Keymap, MarkdownRenderChild, MarkdownRenderer, Plugin, TFile } from 'obsidian';
 import type { EventRef, MarkdownPostProcessorContext } from 'obsidian';
 
 import type { IQuery } from './IQuery';
 import { State } from './Cache';
-import { replaceTaskWithTasks } from './File';
+import { getTaskLineAndFile, replaceTaskWithTasks } from './File';
 import { Query } from './Query/Query';
 import type { GroupHeading } from './Query/GroupHeading';
 import { TaskModal } from './TaskModal';
@@ -303,8 +303,6 @@ class QueryRenderChild extends MarkdownRenderChild {
 
         const link = backLink.createEl('a');
 
-        link.href = task.path;
-        link.setAttribute('data-href', task.path);
         link.rel = 'noopener';
         link.target = '_blank';
         link.addClass('internal-link');
@@ -312,12 +310,6 @@ class QueryRenderChild extends MarkdownRenderChild {
             link.addClass('internal-link-short-mode');
         }
 
-        if (task.precedingHeader !== null) {
-            const sanitisedHeading = task.precedingHeader.replace(/#/g, '');
-            link.href = link.href + '#' + sanitisedHeading;
-            link.setAttribute('data-href', link.getAttribute('data-href') + '#' + sanitisedHeading);
-        }
-
         let linkText: string;
         if (shortMode) {
             linkText = ' 🔗';
@@ -327,6 +319,39 @@ class QueryRenderChild extends MarkdownRenderChild {
 
         link.setText(linkText);
 
+        // Go to the line the task is defined at
+        const vault = this.app.vault;
+        link.addEventListener('click', async (ev: MouseEvent) => {
+            const result = await getTaskLineAndFile(task, vault);
+            if (result) {
+                const [line, file] = result;
+                const leaf = this.app.workspace.getLeaf(Keymap.isModEvent(ev));
+                // This opens the file with the required line highlighted.
+                // It works for Edit and Reading mode, however, for some reason (maybe an Obsidian bug),
+                // when used in Reading mode, switching the result to Edit does not sync the scroll.
+                // A patch suggested over Discord to use leaf.setEphemeralState({scroll: line}) does not seem
+                // to make a difference.
+                // The issue is tracked here: https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1879
+                await leaf.openFile(file, { eState: { line: line } });
+            }
+        });
+
+        link.addEventListener('mousedown', async (ev: MouseEvent) => {
+            // Open in a new tab on middle-click.
+            // This distinction is not available in the 'click' event, so we handle the 'mousedown' event
+            // solely for this.
+            // (for regular left-click we prefer the 'click' event, and not to just do everything here, because
+            // the 'click' event is more generic for touch devices etc.)
+            if (ev.button === 1) {
+                const result = await getTaskLineAndFile(task, vault);
+                if (result) {
+                    const [line, file] = result;
+                    const leaf = this.app.workspace.getLeaf('tab');
+                    await leaf.openFile(file, { eState: { line: line } });
+                }
+            }
+        });
+
         if (!shortMode) {
             backLink.append(')');
         }
diff --git a/yarn.lock b/yarn.lock
index 174950f4..2d909725 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -507,41 +507,11 @@
   resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
   integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
 
-"@codemirror/rangeset@^0.19.5":
-  version "0.19.9"
-  resolved "https://registry.yarnpkg.com/@codemirror/rangeset/-/rangeset-0.19.9.tgz#e80895de93c39dc7899f5be31d368c9d88aa4efc"
-  integrity sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ==
-  dependencies:
-    "@codemirror/state" "^0.19.0"
-
-"@codemirror/state@^0.19.0", "@codemirror/state@^0.19.3", "@codemirror/state@^0.19.6":
-  version "0.19.6"
-  resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-0.19.6.tgz#d631f041d39ce41b7891b099fca26cb1fdb9763e"
-  integrity sha512-sqIQZE9VqwQj7D4c2oz9mfLhlT1ElAzGB5lO1lE33BPyrdNy1cJyCIOecT4cn4VeJOFrnjOeu+IftZ3zqdFETw==
-  dependencies:
-    "@codemirror/text" "^0.19.0"
-
 "@codemirror/state@^6.0.0":
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.1.0.tgz#c0f1d80f61908c9dcf5e2a3fe931e9dd78f3df8a"
   integrity sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==
 
-"@codemirror/text@^0.19.0":
-  version "0.19.5"
-  resolved "https://registry.yarnpkg.com/@codemirror/text/-/text-0.19.5.tgz#75033af2476214e79eae22b81ada618815441c18"
-  integrity sha512-Syu5Xc7tZzeUAM/y4fETkT0zgGr48rDG+w4U38bPwSIUr+L9S/7w2wDE1WGNzjaZPz12F6gb1gxWiSTg9ocLow==
-
-"@codemirror/view@^0.19.31":
-  version "0.19.48"
-  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-0.19.48.tgz#1c657e2b0f8ed896ac6448d6e2215ab115e2a0fc"
-  integrity sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw==
-  dependencies:
-    "@codemirror/rangeset" "^0.19.5"
-    "@codemirror/state" "^0.19.3"
-    "@codemirror/text" "^0.19.0"
-    style-mod "^4.0.0"
-    w3c-keyname "^2.2.4"
-
 "@codemirror/view@^6.2.0":
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.2.0.tgz#bae4c486a84174bd9656af9c9d2bd709d5b26c9f"
@@ -561,6 +531,18 @@
   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.6.tgz#45be4184f00e505411bc265a05e709764114acd8"
   integrity sha512-hqmVU2mUjH6J2ZivHphJ/Pdse2ZD+uGCHK0uvsiLDk/JnSedEVj77CiVUnbMKuU4tih1TZZL8tG9DExQg/GZsw==
 
+"@eslint-community/eslint-utils@^4.2.0":
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
+  integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
+  dependencies:
+    eslint-visitor-keys "^3.3.0"
+
+"@eslint-community/regexpp@^4.4.0":
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724"
+  integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==
+
 "@eslint/eslintrc@^1.3.1":
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.1.tgz#de0807bfeffc37b964a7d0400e0c348ce5a2543d"
@@ -1072,6 +1054,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/semver@^7.3.12":
+  version "7.3.13"
+  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
+  integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
+
 "@types/stack-utils@^2.0.0":
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
@@ -1108,18 +1095,19 @@
   dependencies:
     "@types/yargs-parser" "*"
 
-"@typescript-eslint/eslint-plugin@^5.36.1":
-  version "5.36.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.1.tgz#471f64dc53600025e470dad2ca4a9f2864139019"
-  integrity sha512-iC40UK8q1tMepSDwiLbTbMXKDxzNy+4TfPWgIL661Ym0sD42vRcQU93IsZIrmi+x292DBr60UI/gSwfdVYexCA==
+"@typescript-eslint/eslint-plugin@^5.59.0":
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.0.tgz#c0e10eeb936debe5d1c3433cf36206a95befefd0"
+  integrity sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.36.1"
-    "@typescript-eslint/type-utils" "5.36.1"
-    "@typescript-eslint/utils" "5.36.1"
+    "@eslint-community/regexpp" "^4.4.0"
+    "@typescript-eslint/scope-manager" "5.59.0"
+    "@typescript-eslint/type-utils" "5.59.0"
+    "@typescript-eslint/utils" "5.59.0"
     debug "^4.3.4"
-    functional-red-black-tree "^1.0.1"
+    grapheme-splitter "^1.0.4"
     ignore "^5.2.0"
-    regexpp "^3.2.0"
+    natural-compare-lite "^1.4.0"
     semver "^7.3.7"
     tsutils "^3.21.0"
 
@@ -1141,13 +1129,21 @@
     "@typescript-eslint/types" "5.36.1"
     "@typescript-eslint/visitor-keys" "5.36.1"
 
-"@typescript-eslint/type-utils@5.36.1":
-  version "5.36.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.36.1.tgz#016fc2bff6679f54c0b2df848a493f0ca3d4f625"
-  integrity sha512-xfZhfmoQT6m3lmlqDvDzv9TiCYdw22cdj06xY0obSznBsT///GK5IEZQdGliXpAOaRL34o8phEvXzEo/VJx13Q==
+"@typescript-eslint/scope-manager@5.59.0":
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz#86501d7a17885710b6716a23be2e93fc54a4fe8c"
+  integrity sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==
   dependencies:
-    "@typescript-eslint/typescript-estree" "5.36.1"
-    "@typescript-eslint/utils" "5.36.1"
+    "@typescript-eslint/types" "5.59.0"
+    "@typescript-eslint/visitor-keys" "5.59.0"
+
+"@typescript-eslint/type-utils@5.59.0":
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.0.tgz#8e8d1420fc2265989fa3a0d897bde37f3851e8c9"
+  integrity sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA==
+  dependencies:
+    "@typescript-eslint/typescript-estree" "5.59.0"
+    "@typescript-eslint/utils" "5.59.0"
     debug "^4.3.4"
     tsutils "^3.21.0"
 
@@ -1156,6 +1152,11 @@
   resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.1.tgz#1cf0e28aed1cb3ee676917966eb23c2f8334ce2c"
   integrity sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg==
 
+"@typescript-eslint/types@5.59.0":
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32"
+  integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==
+
 "@typescript-eslint/typescript-estree@5.36.1":
   version "5.36.1"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.1.tgz#b857f38d6200f7f3f4c65cd0a5afd5ae723f2adb"
@@ -1169,17 +1170,32 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/utils@5.36.1":
-  version "5.36.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.36.1.tgz#136d5208cc7a3314b11c646957f8f0b5c01e07ad"
-  integrity sha512-lNj4FtTiXm5c+u0pUehozaUWhh7UYKnwryku0nxJlYUEWetyG92uw2pr+2Iy4M/u0ONMKzfrx7AsGBTCzORmIg==
+"@typescript-eslint/typescript-estree@5.59.0":
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz#8869156ee1dcfc5a95be3ed0e2809969ea28e965"
+  integrity sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==
   dependencies:
+    "@typescript-eslint/types" "5.59.0"
+    "@typescript-eslint/visitor-keys" "5.59.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.59.0":
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.0.tgz#063d066b3bc4850c18872649ed0da9ee72d833d5"
+  integrity sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA==
+  dependencies:
+    "@eslint-community/eslint-utils" "^4.2.0"
     "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.36.1"
-    "@typescript-eslint/types" "5.36.1"
-    "@typescript-eslint/typescript-estree" "5.36.1"
+    "@types/semver" "^7.3.12"
+    "@typescript-eslint/scope-manager" "5.59.0"
+    "@typescript-eslint/types" "5.59.0"
+    "@typescript-eslint/typescript-estree" "5.59.0"
     eslint-scope "^5.1.1"
-    eslint-utils "^3.0.0"
+    semver "^7.3.7"
 
 "@typescript-eslint/visitor-keys@5.36.1":
   version "5.36.1"
@@ -1189,6 +1205,14 @@
     "@typescript-eslint/types" "5.36.1"
     eslint-visitor-keys "^3.3.0"
 
+"@typescript-eslint/visitor-keys@5.59.0":
+  version "5.59.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz#a59913f2bf0baeb61b5cfcb6135d3926c3854365"
+  integrity sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==
+  dependencies:
+    "@typescript-eslint/types" "5.59.0"
+    eslint-visitor-keys "^3.3.0"
+
 abab@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
@@ -2152,10 +2176,10 @@ escodegen@^2.0.0:
   optionalDependencies:
     source-map "~0.6.1"
 
-eslint-config-prettier@^8.5.0:
-  version "8.5.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
-  integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
+eslint-config-prettier@^8.8.0:
+  version "8.8.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
+  integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
 
 eslint-import-resolver-node@^0.3.6:
   version "0.3.6"
@@ -3781,12 +3805,7 @@ mkdirp@^1.0.4:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
-moment@2.29.2:
-  version "2.29.2"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
-  integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
-
-moment@^2.29.4:
+moment@2.29.4, moment@^2.29.4:
   version "2.29.4"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
   integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
@@ -3816,6 +3835,11 @@ natives@^1.1.6:
   resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb"
   integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==
 
+natural-compare-lite@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
+  integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
+
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -3907,15 +3931,13 @@ object.values@^1.1.5:
     define-properties "^1.1.3"
     es-abstract "^1.19.1"
 
-obsidian@0.14.6:
-  version "0.14.6"
-  resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-0.14.6.tgz#010e16da3a1a7725f5e91beb9f14ec8abd00c15d"
-  integrity sha512-oXPJ8Zt10WhN19bk5l4mZuXRZbbdT1QoMgxGGJ0bB7UcJa0bozDzugS5L/QiV9gDoujpUPxDWNVahEel6r0Fpw==
+obsidian@^1.1.1:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.2.5.tgz#86a075de3894303b90450fb98af1293918d13bc3"
+  integrity sha512-RKN4W3PaHsoWwlNRg1SV+iJssQ5vnQYzsCSfmFAUHvA8Q8nzk4pW3HGWXSvor3ZM532KECljG86lEx02OvBwpA==
   dependencies:
-    "@codemirror/state" "^0.19.6"
-    "@codemirror/view" "^0.19.31"
     "@types/codemirror" "0.0.108"
-    moment "2.29.2"
+    moment "2.29.4"
 
 once@^1.3.0:
   version "1.4.0"


Test patch:
diff --git a/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md b/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md
index 4fcfc470..53bd4ad9 100644
--- a/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md
+++ b/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md
@@ -12,7 +12,7 @@ Anything that is involved in producing the built output to users. For example:
 - everything in the "dependencies" (rather than "devDependencies") list in `package.json`
 - `esbuild` (the build system)
 - anything imported by the esbuild config file `esbuild.config.mjs` (for example, `builtin-modules`, `svelte-preprocess`)
-- `obsidian` (also see [[Notes and Special Cases]] below)
+- `obsidian` (also see [[Notes and Special Cases]])
 - all `@codemirror/*`
 - `moment`
 
diff --git a/tests/Group.test.ts b/tests/Group.test.ts
index 827c43ba..86a7e33a 100644
--- a/tests/Group.test.ts
+++ b/tests/Group.test.ts
@@ -5,19 +5,14 @@ import moment from 'moment';
 import { FilenameField } from '../src/Query/Filter/FilenameField';
 import { Group } from '../src/Query/Group';
 import type { Grouper } from '../src/Query/Grouper';
-import type { GroupingProperty } from '../src/Query/Grouper';
 import type { Task } from '../src/Task';
 import { PathField } from '../src/Query/Filter/PathField';
 import { TagsField } from '../src/Query/Filter/TagsField';
+import { FolderField } from '../src/Query/Filter/FolderField';
 import { fromLine } from './TestHelpers';
 
 window.moment = moment;
 
-function checkGroupNamesOfTask(task: Task, property: GroupingProperty, expectedGroupNames: string[]) {
-    const group = Group.getGroupNamesForTask(Group.fromGroupingProperty(property), task);
-    expect(group).toEqual(expectedGroupNames);
-}
-
 describe('Grouping tasks', () => {
     it('groups correctly by path', () => {
         // Arrange
@@ -185,7 +180,7 @@ describe('Grouping tasks', () => {
         });
         const tasks = [t1, t2, t3];
 
-        const grouping: Grouper[] = [Group.fromGroupingProperty('folder'), new FilenameField().createGrouper()];
+        const grouping: Grouper[] = [new FolderField().createGrouper(), new FilenameField().createGrouper()];
 
         // Act
         const groups = Group.by(grouping, tasks);
@@ -218,118 +213,3 @@ describe('Grouping tasks', () => {
         `);
     });
 });
-
-describe('Group names', () => {
-    type GroupNameCase = {
-        groupBy: GroupingProperty;
-        taskLine: string;
-        expectedGroupNames: string[];
-        path?: string;
-        precedingHeading?: string | null;
-    };
-
-    const groupNameCases: Array<GroupNameCase> = [
-        // Maintenance Note: tests are in alphabetical order of 'groupBy' name
-
-        // -----------------------------------------------------------
-        // group by backlink
-        {
-            groupBy: 'backlink',
-            taskLine: '- [ ] xxx', // no location supplied
-            expectedGroupNames: ['Unknown Location'],
-            precedingHeading: 'heading',
-        },
-        {
-            groupBy: 'backlink',
-            taskLine: '- [ ] xxx', // no heading supplied
-            expectedGroupNames: ['c'],
-            path: 'a/b/c.md',
-        },
-        {
-            groupBy: 'backlink',
-            taskLine: '- [ ] xxx',
-            expectedGroupNames: ['c > heading'],
-            path: 'a/b/c.md',
-            precedingHeading: 'heading',
-        },
-        {
-            groupBy: 'backlink',
-            taskLine: '- [ ] xxx',
-            expectedGroupNames: ['c'], // If file name and heading are identical, avoid duplication ('c > c')
-            path: 'a/b/c.md',
-            precedingHeading: 'c',
-        },
-        {
-            groupBy: 'backlink',
-            taskLine: '- [ ] xxx',
-            // underscores in file name component are escaped
-            // but underscores in the heading component are not
-            expectedGroupNames: ['\\_c\\_ > heading _italic text_'],
-            path: 'a/b/_c_.md',
-            precedingHeading: 'heading _italic text_',
-        },
-
-        // -----------------------------------------------------------
-        // group by folder
-        {
-            groupBy: 'folder',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['a/b/'],
-            path: 'a/b/c.md',
-        },
-        {
-            groupBy: 'folder',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['a/\\_b\\_/'], // underscores in folder names are escaped
-            path: 'a/_b_/c.md',
-        },
-        {
-            // file in root of vault:
-            groupBy: 'folder',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['/'],
-            path: 'a.md',
-        },
-
-        // -----------------------------------------------------------
-        // group by root
-        {
-            groupBy: 'root',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['a/'],
-            path: 'a/b/c.md',
-        },
-        {
-            groupBy: 'root',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['\\_g\\_/'], // underscores in root folder names are escaped
-            path: '_g_/h/i.md',
-        },
-        {
-            groupBy: 'root',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['a/'],
-            path: 'a\\b\\c.md', // Windows path
-        },
-        {
-            // file in root of vault:
-            groupBy: 'root',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['/'],
-            path: 'a.md',
-        },
-        // -----------------------------------------------------------
-    ];
-
-    test.concurrent.each<GroupNameCase>(groupNameCases)(
-        'assigns correct group name (%j)',
-        ({ groupBy, taskLine, path, expectedGroupNames, precedingHeading }) => {
-            const task = fromLine({
-                line: taskLine,
-                path: path ? path : '',
-                precedingHeader: precedingHeading,
-            });
-            checkGroupNamesOfTask(task, groupBy, expectedGroupNames);
-        },
-    );
-});
diff --git a/tests/Query/Filter/BacklinkField.test.ts b/tests/Query/Filter/BacklinkField.test.ts
new file mode 100644
index 00000000..21352946
--- /dev/null
+++ b/tests/Query/Filter/BacklinkField.test.ts
@@ -0,0 +1,69 @@
+import { BacklinkField } from '../../../src/Query/Filter/BacklinkField';
+import { fromLine } from '../../TestHelpers';
+
+describe('backlink', () => {
+    it('should provide the backlink', () => {
+        const field = new BacklinkField();
+
+        expect(field.value(fromLine({ line: '- [ ] do' }))).toStrictEqual('Unknown Location');
+        expect(field.value(fromLine({ line: '- [ ] do', path: 'folder/file.md' }))).toStrictEqual('file');
+        expect(field.value(fromLine({ line: '- [ ] do', path: 'a_b/_c_d_/_fi_le_.md' }))).toStrictEqual('_fi_le_');
+        expect(field.value(fromLine({ line: '- [ ] do', path: 'file.md', precedingHeader: 'topic' }))).toStrictEqual(
+            'file > topic',
+        );
+        expect(
+            field.value(fromLine({ line: '- [ ] do', path: 'fi_le.md', precedingHeader: 'topic _ita_' })),
+        ).toStrictEqual('fi_le > topic _ita_');
+    });
+});
+
+describe('backlink', () => {
+    it('should not support filtering', () => {
+        // Arrange
+        const field = new BacklinkField();
+
+        // Assert
+        expect(field.createFilterOrErrorMessage('backlink includes heading > filename')).not.toBeValid();
+        expect(field.canCreateFilterForLine('backlink includes heading > filename')).toEqual(false);
+    });
+});
+
+describe('grouping by backlink', () => {
+    it('supports grouping methods correctly', () => {
+        const field = new BacklinkField();
+        expect(field.supportsGrouping()).toEqual(true);
+
+        const fieldGrouper = field.createGrouper();
+        expect(fieldGrouper.property).toEqual('backlink');
+    });
+
+    it.each([
+        // no location supplied
+        [undefined, 'heading', ['Unknown Location']],
+
+        // no heading supplied
+        ['a/b/c.md', undefined, ['c']],
+
+        // File and heading, nominal case
+        ['a/b/c.md', 'heading', ['c > heading']],
+
+        // If file name and heading are identical, avoid duplication ('c > c')
+        ['a/b/c.md', 'c', ['c']],
+
+        // Underscores in file name component are escaped
+        ['a/b/_c_.md', undefined, ['\\_c\\_']],
+
+        // But underscores in the heading component are not
+        ['a/b/_c_.md', 'heading _italic text_', ['\\_c\\_ > heading _italic text_']],
+    ])(
+        'task "%s" with path "%s" should have groups: %s',
+        (path: string | undefined, heading: string | undefined, groups: string[]) => {
+            // Arrange
+            const grouper = new BacklinkField().createGrouper().grouper;
+            const t = '- [ ] xyz';
+
+            // Assert
+            expect(grouper(fromLine({ line: t, path: path, precedingHeader: heading }))).toEqual(groups);
+        },
+    );
+});
diff --git a/tests/Query/Filter/FolderField.test.ts b/tests/Query/Filter/FolderField.test.ts
new file mode 100644
index 00000000..1ebdcd20
--- /dev/null
+++ b/tests/Query/Filter/FolderField.test.ts
@@ -0,0 +1,55 @@
+import { FolderField } from '../../../src/Query/Filter/FolderField';
+import { fromLine } from '../../TestHelpers';
+
+describe('folder', () => {
+    it('should provide access to the folder', () => {
+        const field = new FolderField();
+
+        expect(field.value(fromLine({ line: '- [ ] do' }))).toStrictEqual('/');
+        expect(field.value(fromLine({ line: '- [ ] do', path: 'outside/inside/file.md' }))).toStrictEqual(
+            'outside/inside/',
+        );
+        expect(field.value(fromLine({ line: '- [ ] do', path: 'a_b/_c_d_/file.md' }))).toStrictEqual('a_b/_c_d_/');
+    });
+});
+
+describe('folder', () => {
+    // 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('filter by folder (includes)', () => {
+        // Arrange
+        const filter = new FolderField().createFilterOrErrorMessage('folder includes search_text');
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).not.toMatchTaskWithPath('');
+        expect(filter).toMatchTaskWithPath('/some/SeArch_Text/some file name.md');
+        expect(filter).not.toMatchTaskWithPath('/other/folder/search_text.md'); // Ignores text in file names
+    });
+});
+
+describe('grouping by folder', () => {
+    it('supports grouping methods correctly', () => {
+        const field = new FolderField();
+        expect(field.supportsGrouping()).toEqual(true);
+
+        const fieldGrouper = field.createGrouper();
+        expect(fieldGrouper.property).toEqual('folder');
+    });
+
+    it.each([
+        ['- [ ] a', 'a/b/c.md', ['a/b/']],
+        // underscores in folder names are escaped
+        ['- [ ] a', 'a/_b_/c.md', ['a/\\_b\\_/']],
+        // file in root of vault:
+        ['- [ ] a', 'a.md', ['/']],
+    ])('task "%s" with path "%s" should have groups: %s', (taskLine: string, path: string, groups: string[]) => {
+        // Arrange
+        const grouper = new FolderField().createGrouper().grouper;
+
+        // Assert
+        expect(grouper(fromLine({ line: taskLine, path: path }))).toEqual(groups);
+    });
+});
diff --git a/tests/Query/Filter/RootField.test.ts b/tests/Query/Filter/RootField.test.ts
new file mode 100644
index 00000000..78754e95
--- /dev/null
+++ b/tests/Query/Filter/RootField.test.ts
@@ -0,0 +1,58 @@
+import { RootField } from '../../../src/Query/Filter/RootField';
+import { fromLine } from '../../TestHelpers';
+
+describe('root', () => {
+    it('should provide access to root folder name with trailing slash', () => {
+        const field = new RootField();
+
+        const line = '- [ ] do';
+        expect(field.value(fromLine({ line: line }))).toStrictEqual('/');
+        expect(field.value(fromLine({ line: line, path: 'outside/inside/A.md' }))).toStrictEqual('outside/');
+        expect(field.value(fromLine({ line: line, path: 'a_b/_c_d_/B.md' }))).toStrictEqual('a_b/');
+        expect(field.value(fromLine({ line: line, path: '/root/SeArch_Text/search_text.md' }))).toStrictEqual('root/');
+    });
+});
+
+describe('root', () => {
+    // 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('filter by root (includes)', () => {
+        // Arrange
+        const filter = new RootField().createFilterOrErrorMessage('root includes search_text');
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).not.toMatchTaskWithPath('');
+        expect(filter).toMatchTaskWithPath('/SeArch_Text/some folder name/some file name.md');
+        expect(filter).not.toMatchTaskWithPath('/some root folder/SeArch_Text/search_text.md'); // Ignores text in child folder names
+        expect(filter).not.toMatchTaskWithPath('/some root folder/folder/search_text.md'); // Ignores text in file names
+    });
+});
+
+describe('grouping by root', () => {
+    it('supports grouping methods correctly', () => {
+        const field = new RootField();
+        expect(field.supportsGrouping()).toEqual(true);
+
+        const fieldGrouper = field.createGrouper();
+        expect(fieldGrouper.property).toEqual('root');
+    });
+
+    it.each([
+        ['- [ ] a', 'a/b/c.md', ['a/']],
+        // underscores in root folder names are escaped
+        ['- [ ] a', '_g_/h/i.md', ['\\_g\\_/']],
+        // Windows path
+        ['- [ ] a', 'a\\b\\c.md', ['a/']],
+        // file in root of vault:
+        ['- [ ] a', 'a.md', ['/']],
+    ])('task "%s" with path "%s" should have groups: %s', (taskLine: string, path: string, groups: string[]) => {
+        // Arrange
+        const grouper = new RootField().createGrouper().grouper;
+
+        // Assert
+        expect(grouper(fromLine({ line: taskLine, path: path }))).toEqual(groups);
+    });
+});