Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1773 (2023-03-21 00:28:22)

refactor: Make Editor Suggestions more modular
<!--- Provide a general summary of your changes in the Title above -->

# Description

This is the final PR broken out from #1703

This PR is one in a collection of PRs with the ultimate goal of providing an _option_ to write tasks using a plaintext version of this plugin's task format.

This PR's goal is to pave the way for implementing that plaintext task format, specifically by:

* **Extending  the `TaskFormat` abstraction (introduced in #1728) with a new field `buildSuggestions`** that provides inline suggestions while a user is writing a Task.
* Refactoring the existing `buildSuggestions` function to accept an instance of `DefaultTaskSerializerSymbols`.

### Implementation 

The core of this PR is the concept of a `TaskFormat` which encapsulates all of the format specific ways that a `Task` is written to and read from a Markdown File. This PR extends the interface with the `buildSuggestions` field. 

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/fdde76e171f56b91d8e99954e292e41a8635e002/src/Config/Settings.ts#L24-L31

#### SuggestionBuilder

A `SuggestionBuilder` is responsible for generating the suggestion list as a user is typing. This abstraction is directly modeled after the existing `buildSuggestions` function.

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/fdde76e171f56b91d8e99954e292e41a8635e002/src/Suggestor/index.ts#L18-L21

## Motivation and Context

This PR completes the effort to add  an abstration barrier (`TaskFormat`) which encapsulates how Tasks are written to files.

In effort of more easily supporting alternate task formats.

## How has this been tested?

This PR changes no user facing functionality and leverages the existing test suite to validate that.

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [ ] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [ ] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [ ] **Documentation** (prefix: `docs` - improvements to any documentation content)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))

Internal changes:

- [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`.
- [ ] 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)



Patch:
diff --git a/src/Config/Settings.ts b/src/Config/Settings.ts
index 54ae2e2a..e865af4c 100644
--- a/src/Config/Settings.ts
+++ b/src/Config/Settings.ts
@@ -1,7 +1,9 @@
+import { makeDefaultSuggestionBuilder } from '../Suggestor/Suggestor';
 import { DEFAULT_SYMBOLS } from '../TaskSerializer/DefaultTaskSerializer';
 import { StatusConfiguration } from '../StatusConfiguration';
 import { Status } from '../Status';
 import { DefaultTaskSerializer, type TaskSerializer } from '../TaskSerializer';
+import type { SuggestionBuilder } from '../Suggestor';
 import { DebugSettings } from './DebugSettings';
 import { StatusSettings } from './StatusSettings';
 import { Feature } from './Feature';
@@ -24,11 +26,17 @@ interface TaskFormat {
     displayName: string;
     /** {@link TaskSerializer} responsible for reading Tasks from text and writing them back into text */
     taskSerializer: TaskSerializer;
+    /** Function that generates Intellisense-like suggestions as a user is typing a Task */
+    buildSuggestions?: SuggestionBuilder;
 }
 
 /** Map of all defined {@link TaskFormat}s */
 export const TASK_FORMATS = {
-    tasksPluginEmoji: { displayName: 'Default', taskSerializer: new DefaultTaskSerializer(DEFAULT_SYMBOLS) },
+    tasksPluginEmoji: {
+        displayName: 'Default',
+        taskSerializer: new DefaultTaskSerializer(DEFAULT_SYMBOLS),
+        buildSuggestions: makeDefaultSuggestionBuilder(DEFAULT_SYMBOLS),
+    },
 } as const;
 
 export type TASK_FORMATS = typeof TASK_FORMATS; // For convenience to make some typing easier
diff --git a/src/File.ts b/src/File.ts
index 2e450749..ede8f2ee 100644
--- a/src/File.ts
+++ b/src/File.ts
@@ -1,7 +1,7 @@
-import { type ListItemCache, MarkdownView, MetadataCache, Notice, TFile, Vault, Workspace } from 'obsidian';
+import { MarkdownView, MetadataCache, Notice, TFile, Vault, Workspace } from 'obsidian';
+import type { ListItemCache } from 'obsidian';
 
 import { getSettings } from './Config/Settings';
-import { type MockListItemCache, type MockTask, saveMockDataForTesting } from './lib/MockDataCreator';
 import type { Task } from './Task';
 
 let metadataCache: MetadataCache | undefined;
@@ -12,8 +12,6 @@ let workspace: Workspace | undefined;
 const supportedFileExtensions = ['md'];
 const supportedViewTypes = [MarkdownView];
 
-export type ErrorLoggingFunction = (message: string) => void;
-
 export const initializeFile = ({
     metadataCache: newMetadataCache,
     vault: newVault,
@@ -152,60 +150,11 @@ const tryRepetitive = async ({
     const fileContent = await vault.read(file); // TODO: replace with vault.process.
     const fileLines = fileContent.split('\n');
 
-    const taskLineNumber = findLineNumberOfTaskToToggle(originalTask, fileLines, listItemsCache, errorAndNotice);
-
-    if (taskLineNumber === undefined) {
-        const logDataForMocking = false;
-        if (logDataForMocking) {
-            // There was an error finding the correct line to toggle,
-            // 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);
-        }
-        errorAndNotice('Tasks: could not find task to toggle in the file.');
-        return;
-    }
-
-    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'));
-};
-
-/**
- * Try to find the line number of the originalTask
- * @param originalTask - the {@link Task} line that the user clicked on
- * @param fileLines - the lines read from the file.
- * @param listItemsCache
- * @param errorLoggingFunction - a function of type {@link ErrorLoggingFunction} - which will be called if the found
- *                               line differs from the original markdown in {@link originalTask}.
- *                               This parameter is provided to allow tests to be written for this code
- *                               that do not display a popup warning, but instead capture the error message.
- */
-export function findLineNumberOfTaskToToggle(
-    originalTask: Task | MockTask,
-    fileLines: string[],
-    listItemsCache: ListItemCache[] | MockListItemCache[],
-    errorLoggingFunction: ErrorLoggingFunction,
-) {
-    const fileLinesCount = fileLines.length;
     const { globalFilter } = getSettings();
-    let taskLineNumber: number | undefined;
+    let listItem: ListItemCache | undefined;
     let sectionIndex = 0;
     for (const listItemCache of listItemsCache) {
-        const listItemLineNumber = listItemCache.position.start.line;
-        if (listItemLineNumber >= fileLinesCount) {
-            // One or more lines has been deleted since the cache was populated,
-            // so there is at least one list item in the cache that is beyond
-            // the end of the actual file on disk.
-            return undefined;
-        }
-
-        if (listItemLineNumber < originalTask.taskLocation.sectionStart) {
+        if (listItemCache.position.start.line < originalTask.sectionStart) {
             continue;
         }
 
@@ -213,16 +162,16 @@ export function findLineNumberOfTaskToToggle(
             continue;
         }
 
-        const line = fileLines[listItemLineNumber];
+        const line = fileLines[listItemCache.position.start.line];
         if (line.includes(globalFilter)) {
-            if (sectionIndex === originalTask.taskLocation.sectionIndex) {
+            if (sectionIndex === originalTask.sectionIndex) {
                 if (line === originalTask.originalMarkdown) {
-                    taskLineNumber = listItemLineNumber;
+                    listItem = listItemCache;
                 } else {
-                    errorLoggingFunction(
-                        `Tasks: Unable to find task in file ${originalTask.taskLocation.path}.
+                    errorAndNotice(
+                        `Tasks: Unable to find task in file ${originalTask.path}.
 Expected task:
-${originalTask.originalMarkdown}
+${originalTask.toFileLineString()}
 Found task:
 ${line}`,
                     );
@@ -234,5 +183,16 @@ ${line}`,
             sectionIndex++;
         }
     }
-    return taskLineNumber;
-}
+    if (listItem === undefined) {
+        errorAndNotice('Tasks: could not find task to toggle in the file.');
+        return;
+    }
+
+    const updatedFileLines = [
+        ...fileLines.slice(0, listItem.position.start.line),
+        ...newTasks.map((task: Task) => task.toFileLineString()),
+        ...fileLines.slice(listItem.position.start.line + 1), // Only supports single-line tasks.
+    ];
+
+    await vault.modify(file, updatedFileLines.join('\n'));
+};
diff --git a/src/Suggestor/EditorSuggestorPopup.ts b/src/Suggestor/EditorSuggestorPopup.ts
index 1c8de52d..27d2be0f 100644
--- a/src/Suggestor/EditorSuggestorPopup.ts
+++ b/src/Suggestor/EditorSuggestorPopup.ts
@@ -1,10 +1,9 @@
 import { App, Editor, EditorSuggest, TFile } from 'obsidian';
 import type { EditorPosition, EditorSuggestContext, EditorSuggestTriggerInfo } from 'obsidian';
 
-import type { Settings } from '../Config/Settings';
+import { type Settings, getUserSelectedTaskFormat } from '../Config/Settings';
 import * as task from '../Task';
-import { buildSuggestions } from './Suggestor';
-import type { SuggestInfo } from './Suggestor';
+import type { SuggestInfo } from '.';
 
 export type SuggestInfoWithContext = SuggestInfo & {
     context: EditorSuggestContext;
@@ -38,13 +37,11 @@ export class EditorSuggestor extends EditorSuggest<SuggestInfoWithContext> {
         const line = context.query;
         const currentCursor = context.editor.getCursor();
 
-        const suggestions: SuggestInfo[] = buildSuggestions(line, currentCursor.ch, this.settings);
+        const suggestions: SuggestInfo[] =
+            getUserSelectedTaskFormat().buildSuggestions?.(line, currentCursor.ch, this.settings) ?? [];
 
         // Add the editor context to all the suggestions
-        const suggestionsWithContext: SuggestInfoWithContext[] = [];
-        for (const suggestion of suggestions) suggestionsWithContext.push({ ...suggestion, context: context });
-
-        return suggestionsWithContext;
+        return suggestions.map((s) => ({ ...s, context }));
     }
 
     renderSuggestion(value: SuggestInfoWithContext, el: HTMLElement) {
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index b293b50b..09c42f30 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -1,96 +1,86 @@
 import type { Settings } from '../Config/Settings';
-import { TASK_FORMATS } from '../Config/Settings';
 import { DateParser } from '../Query/DateParser';
 import { doAutocomplete } from '../DateAbbreviations';
 import { Recurrence } from '../Recurrence';
+import type { DefaultTaskSerializerSymbols } from '../TaskSerializer/DefaultTaskSerializer';
 import { TaskRegularExpressions } from '../Task';
-const { symbols } = TASK_FORMATS.tasksPluginEmoji.taskSerializer;
-const datePrefixCharacters = `${symbols.startDateSymbol}${symbols.scheduledDateSymbol}${symbols.dueDateSymbol}`;
+import type { SuggestInfo, SuggestionBuilder } from '.';
 
-/*
- * A suggestion presented to the user and some metadata about it.
- */
-export type SuggestInfo = {
-    suggestionType?: 'match' | 'default' | 'empty';
-    // What to display to the user
-    displayText: string;
-    // What to append to the note
-    appendText: string;
-    // At what index in the line to do the insertion (if not specified, the cursor location is used)
-    insertAt?: number;
-    // How many characters to skip from the original line (e.g. if replacing existing text)
-    insertSkip?: number;
-};
-
-/*
- * Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
- */
-export function buildSuggestions(line: string, cursorPos: number, settings: Settings): SuggestInfo[] {
-    let suggestions: SuggestInfo[] = [];
+export function makeDefaultSuggestionBuilder(symbols: DefaultTaskSerializerSymbols): SuggestionBuilder {
+    const datePrefixCharacters = `${symbols.startDateSymbol}${symbols.scheduledDateSymbol}${symbols.dueDateSymbol}`;
+    /*
+     * Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
+     */
+    return (line: string, cursorPos: number, settings: Settings): SuggestInfo[] => {
+        let suggestions: SuggestInfo[] = [];
 
-    // Step 1: add date suggestions if relevant
-    suggestions = suggestions.concat(addDatesSuggestions(line, cursorPos, settings));
+        // Step 1: add date suggestions if relevant
+        suggestions = suggestions.concat(addDatesSuggestions(line, cursorPos, settings, datePrefixCharacters));
 
-    // Step 2: add recurrence suggestions if relevant
-    suggestions = suggestions.concat(addRecurrenceSuggestions(line, cursorPos, settings));
+        // Step 2: add recurrence suggestions if relevant
+        suggestions = suggestions.concat(addRecurrenceSuggestions(line, cursorPos, settings, symbols.recurrenceSymbol));
 
-    // Step 3: add more general suggestions ('due', 'recurrence' etc)
-    const morePossibleSuggestions = getPossibleComponentSuggestions(line, settings);
-    // We now filter the general suggestions according to the word at the cursor. If there's
-    // something to match, we filter the suggestions accordingly, so the user can get more specific
-    // results according to what she's typing.
-    // If there's no good match, present the suggestions as they are
-    const wordMatch = matchByPosition(line, /([a-zA-Z'_-]*)/g, cursorPos);
-    let addedSuggestions = false;
-    if (wordMatch && wordMatch.length > 0) {
-        const wordUnderCursor = wordMatch[0];
-        if (wordUnderCursor.length >= Math.max(1, settings.autoSuggestMinMatch)) {
-            const filteredSuggestions = morePossibleSuggestions.filter((suggestInfo) =>
-                suggestInfo.displayText.toLowerCase().includes(wordUnderCursor.toLowerCase()),
-            );
-            for (const filtered of filteredSuggestions) {
-                suggestions.push({
-                    suggestionType: 'match',
-                    displayText: filtered.displayText,
-                    appendText: filtered.appendText,
-                    insertAt: wordMatch.index,
-                    insertSkip: wordUnderCursor.length,
-                });
-                addedSuggestions = true;
+        // Step 3: add more general suggestions ('due', 'recurrence' etc)
+        const morePossibleSuggestions = getPossibleComponentSuggestions(line, settings, symbols);
+        // We now filter the general suggestions according to the word at the cursor. If there's
+        // something to match, we filter the suggestions accordingly, so the user can get more specific
+        // results according to what she's typing.
+        // If there's no good match, present the suggestions as they are
+        const wordMatch = matchByPosition(line, /([a-zA-Z'_-]*)/g, cursorPos);
+        let addedSuggestions = false;
+        if (wordMatch && wordMatch.length > 0) {
+            const wordUnderCursor = wordMatch[0];
+            if (wordUnderCursor.length >= Math.max(1, settings.autoSuggestMinMatch)) {
+                const filteredSuggestions = morePossibleSuggestions.filter((suggestInfo) =>
+                    suggestInfo.displayText.toLowerCase().includes(wordUnderCursor.toLowerCase()),
+                );
+                for (const filtered of filteredSuggestions) {
+                    suggestions.push({
+                        suggestionType: 'match',
+                        displayText: filtered.displayText,
+                        appendText: filtered.appendText,
+                        insertAt: wordMatch.index,
+                        insertSkip: wordUnderCursor.length,
+                    });
+                    addedSuggestions = true;
+                }
             }
         }
-    }
-    // That's where we're adding all the suggestions in case there's nothing specific to match
-    // (and we're allowed by the settings to bring back a zero-sized match)
-    if (!addedSuggestions && settings.autoSuggestMinMatch === 0)
-        suggestions = suggestions.concat(morePossibleSuggestions);
+        // That's where we're adding all the suggestions in case there's nothing specific to match
+        // (and we're allowed by the settings to bring back a zero-sized match)
+        if (!addedSuggestions && settings.autoSuggestMinMatch === 0)
+            suggestions = suggestions.concat(morePossibleSuggestions);
 
-    // Unless we have a suggestion that is a match for something the user is currently typing, add
-    // an 'Enter' entry in the beginning of the menu, so an Enter press will move to the next line
-    // rather than insert a suggestion
-    if (suggestions.length > 0 && !suggestions.some((value) => value.suggestionType === 'match')) {
-        // No actual match, only default ones
-        suggestions.unshift({
-            suggestionType: 'empty',
-            displayText: '⏎',
-            appendText: '\n',
-        });
-    }
-
-    // Either way, after all the aggregations above, never suggest more than the max items
-    suggestions = suggestions.slice(0, settings.autoSuggestMaxItems);
+        // Unless we have a suggestion that is a match for something the user is currently typing, add
+        // an 'Enter' entry in the beginning of the menu, so an Enter press will move to the next line
+        // rather than insert a suggestion
+        if (suggestions.length > 0 && !suggestions.some((value) => value.suggestionType === 'match')) {
+            // No actual match, only default ones
+            suggestions.unshift({
+                suggestionType: 'empty',
+                displayText: '⏎',
+                appendText: '\n',
+            });
+        }
 
-    return suggestions;
-}
+        // Either way, after all the aggregations above, never suggest more than the max items
+        suggestions = suggestions.slice(0, settings.autoSuggestMaxItems);
 
-function hasPriority(line: string) {
-    if (Object.values(symbols.prioritySymbols).some((value) => value.length > 0 && line.includes(value))) return true;
+        return suggestions;
+    };
 }
 
 /*
  * Get suggestions for generic task components, e.g. a priority or a 'due' symbol
  */
-function getPossibleComponentSuggestions(line: string, _settings: Settings): SuggestInfo[] {
+function getPossibleComponentSuggestions(
+    line: string,
+    _settings: Settings,
+    symbols: DefaultTaskSerializerSymbols,
+): SuggestInfo[] {
+    const hasPriority = (line: string) =>
+        Object.values(symbols.prioritySymbols).some((value) => value.length > 0 && line.includes(value));
+
     const suggestions: SuggestInfo[] = [];
 
     if (!line.includes(symbols.dueDateSymbol))
@@ -139,7 +129,12 @@ function getPossibleComponentSuggestions(line: string, _settings: Settings): Sug
  * Generic predefined suggestions, in turn, also have two options: either filtered (if the user started typing
  * something where a date is expected) or unfiltered
  */
-function addDatesSuggestions(line: string, cursorPos: number, settings: Settings): SuggestInfo[] {
+function addDatesSuggestions(
+    line: string,
+    cursorPos: number,
+    settings: Settings,
+    datePrefixCharacters: string,
+): SuggestInfo[] {
     const genericSuggestions = [
         'today',
         'tomorrow',
@@ -224,7 +219,7 @@ function addDatesSuggestions(line: string, cursorPos: number, settings: Settings
  * Generic predefined suggestions, in turn, also have two options: either filtered (if the user started typing
  * something where a recurrence is expected) or unfiltered
  */
-function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Settings) {
+function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Settings, recurrenceSymbol: string) {
     const genericSuggestions = [
         'every',
         'every day',
@@ -242,7 +237,7 @@ function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Set
     ];
 
     const results: SuggestInfo[] = [];
-    const recurrenceRegex = new RegExp(`(${symbols.recurrenceSymbol})\\s*([0-9a-zA-Z ]*)`, 'ug');
+    const recurrenceRegex = new RegExp(`(${recurrenceSymbol})\\s*([0-9a-zA-Z ]*)`, 'ug');
     const recurrenceMatch = matchByPosition(line, recurrenceRegex, cursorPos);
     if (recurrenceMatch && recurrenceMatch.length >= 2) {
         const recurrencePrefix = recurrenceMatch[1];
diff --git a/src/Suggestor/index.ts b/src/Suggestor/index.ts
new file mode 100644
index 00000000..1c135a1a
--- /dev/null
+++ b/src/Suggestor/index.ts
@@ -0,0 +1,21 @@
+import type { Settings } from '../Config/Settings';
+
+/*
+ * A suggestion presented to the user and some metadata about it.
+ */
+export type SuggestInfo = {
+    suggestionType?: 'match' | 'default' | 'empty';
+    // What to display to the user
+    displayText: string;
+    // What to append to the note
+    appendText: string;
+    // At what index in the line to do the insertion (if not specified, the cursor location is used)
+    insertAt?: number;
+    // How many characters to skip from the original line (e.g. if replacing existing text)
+    insertSkip?: number;
+};
+
+/*
+ * Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
+ */
+export type SuggestionBuilder = (line: string, cursorPos: number, settings: Settings) => SuggestInfo[];
diff --git a/src/lib/MockDataCreator.ts b/src/lib/MockDataCreator.ts
deleted file mode 100644
index 902d6cca..00000000
--- a/src/lib/MockDataCreator.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import type { ListItemCache, Pos } from 'obsidian';
-import type { Task } from '../Task';
-
-type MockTaskLocation = {
-    path: string;
-    lineNumber: number;
-    sectionStart: number;
-    sectionIndex: number;
-    precedingHeader: string | null;
-};
-
-/** a mock for {@link ListItemCache.task} */
-type MockListItemCacheTask = string | undefined;
-
-/** a mock for {@link Pos} */
-type MockPos = Pos;
-
-/** a mock for {@link ListItemCache} */
-export type MockListItemCache = { task: string | undefined; position: Pos };
-type MockListItemCaches = MockListItemCache[];
-
-/** a mock for {@link Task} */
-export type MockTask = {
-    originalMarkdown: string;
-    taskLocation: MockTaskLocation;
-};
-
-/** All the data required to call {@link findLineNumberOfTaskToToggle} */
-export type MockTogglingDataForTesting = {
-    cacheData: { listItemsCache: MockListItemCache[] };
-    fileData: { fileLines: string[] };
-    taskData: MockTask;
-};
-
-/**
- * This function can be used to save data that is used
- * when finding which line to toggle in a file.
- * @param originalTask
- * @param fileLines
- * @param listItemsCache
- *
- * @see saveMockDataForTesting
- */
-export function getMockDataForTesting(
-    originalTask: Task,
-    fileLines: string[],
-    listItemsCache: ListItemCache[],
-): MockTogglingDataForTesting {
-    const allDataFromListItemCache: MockListItemCaches = [];
-    for (const listItemCache of listItemsCache) {
-        const pos: MockPos = listItemCache.position;
-        const task: MockListItemCacheTask = listItemCache.task;
-        const dataFromListItemCache: MockListItemCache = {
-            position: pos,
-            task: task,
-        };
-        allDataFromListItemCache.push(dataFromListItemCache);
-    }
-    const mockTaskLocation: MockTaskLocation = {
-        path: originalTask.taskLocation.path,
-        lineNumber: originalTask.taskLocation.lineNumber,
-        sectionStart: originalTask.taskLocation.sectionStart,
-        sectionIndex: originalTask.taskLocation.sectionIndex,
-        precedingHeader: originalTask.taskLocation.precedingHeader,
-    };
-    return {
-        taskData: {
-            originalMarkdown: originalTask.originalMarkdown,
-            taskLocation: mockTaskLocation,
-        },
-        fileData: {
-            fileLines: fileLines,
-        },
-        cacheData: {
-            listItemsCache: allDataFromListItemCache,
-        },
-    };
-}
-
-/**
- * Write the supplied data to the console, so it can be saved for use in testing.
- *
- * @param originalTask
- * @param fileLines
- * @param listItemsCache
- */
-export function saveMockDataForTesting(originalTask: Task, fileLines: string[], listItemsCache: ListItemCache[]) {
-    const everything = getMockDataForTesting(originalTask, fileLines, listItemsCache);
-    console.error(`Inconsistent lines: SAVE THE OUTPUT
-data:
-${JSON.stringify(everything)}
-`);
-}


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/1680 - Reading Mode line numbers not updated on editing.md b/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/1680 - Reading Mode line numbers not updated on editing.md
index c51d564e..6348cb75 100644
--- a/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/1680 - Reading Mode line numbers not updated on editing.md
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/1680 - Reading Mode line numbers not updated on editing.md
@@ -2,91 +2,32 @@
 
 ## Instructions
 
-> [!Tip] Goal
-> **Given** a file is open in both two panes:
->
-> - an editing mode (Source or Live Preview)
-> - Reading mode
->
-> **When** I add or remove lines from the file
->
-> **Then** rendered tasks in the Reading pane should be updated, so that when I toggle them, the correct line is toggled.
+**Given** a file is open in both two panes:
 
-## Testing steps
+- an editing mode (Source or Live Preview)
+- Reading mode
 
-> [!Todo] Testing Steps
->
-> - 1 **Setup**
->   - Open this file in Source View
->   - Open it in a second pane, in Reading View
-> - 2 **Add some white space before the tasks**
->   - After the line containing `In step 2`, enter 5 blank lines
-> - 3 **Toggle a task in each section, in the Reading View pane**
->   - Toggle `Section 1/Task 1` to Done then to TODO
->   - Toggle `Section 2/Task 2` to Done then to TODO
-> - 4 **Quickly delete the blank lines then toggle `Section 2/Task 1`**
->   - Select the 5 added lines
->   - Move the cursor next to the checkbox of `Section 2/Task 1`
->   - Hit the delete key to delete the blank lines
->   - **As quickly as possible** (and in under 2 seconds) **toggle `Section 2/Task 1`**
->   - **As quickly as possible** (and in under 2 seconds) **toggle `Section 2/Task 2`**
+**When** I add or remove lines from the file
 
-### Expected result
+**Then** rendered tasks in the Reading pane should be updated, so that when I toggle them, the correct line is toggled.
 
-> [!Success]
->
-> - `Section 2/Task 1` is marked as Done
-> - `Section 2/Task 2` is marked as Done
+## Tasks Section 1
 
----
-
-## Test Data
-
-### Tasks Section 1
-
-In step 2, Insert 5 blank lines after this line.
-
-Some random tasks:
+Some random tasks
 
 - [ ] #task Section 1/Task 1
 - [ ] #task Section 1/Task 2
 
-### Tasks Section 2
+## Tasks Section 2
 
-Some more random tasks:
+Some more random tasks
 
 - [ ] #task Section 2/Task 1
 - [ ] #task Section 2/Task 2
 
----
-
-## Log of actual results - newest first
-
-### Actual result in `5649417`, PR #1702, 2023-02-27
-
-<https://github.com/obsidian-tasks-group/obsidian-tasks/commit/5649417006a5ccf80cdaaaeb2bc404db5c6f6a86>
-
-> [!Bug]
->
-> - Two console error messages:
->
-> ```text
-> plugin:obsidian-tasks-plugin:17532 Tasks: could not find task to toggle in the file.
-> plugin:obsidian-tasks-plugin:17532 Tasks: could not find task to toggle in the file.```
-> ```
-
-### Actual result in Tasks 1.25.0
+## Query results
 
-> [!Bug]
->
-> - **Step 3**
->   - Sometimes the task line that was toggled is copied over an earlier task line in the file, losing that original line
-> - **Step 4**
->   - `Section 2/Task 1` is marked as Done
->   - Toggling `Section 2/Task 1`:
->     - sometimes works, but Obsidian shows a notice about the file being modified externally, and changes are being merged
->     - sometimes fails, giving console message:
->
-> ```text
-> Tasks: could not find task to toggle in the file.
-> ```
+```tasks
+path includes 1680 - Reading Mode line numbers not updated on editing
+sort by description
+```
diff --git a/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/Embed Task in to Note.md b/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/Embed Task in to Note.md
index 8086d20b..68a928ed 100644
--- a/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/Embed Task in to Note.md
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Task Toggling Scenarios/Embed Task in to Note.md
@@ -1,24 +1,32 @@
 # Embed Task in to Note
 
-## Scenario: Embedding individual tasks
-
-- **Given** a bullet list which embeds a task via a block reference, in the same file or another file
-- **When** the embedded task is toggled
-- **Then** there is an error message saying that the task with the block reference could not be found, to be toggled
-  - See [Issue #688](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/688)
+See [When a block-referenced task is made complete, it is completed after rewriting the contents of another task. · Issue #688](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/688)
 
 ## Category 1
 
 - [ ] #task task1a
 - [ ] #task task1b
+- [ ] #task task1c
 
 ## Category 2
 
 - [ ] #task task2a
 - [ ] #task task2b ^ca47c7
+- [ ] #task task2c
+
+## Scenario 3: Embedding individual tasks
+
+- **Given** A bullet list which embeds a task via a block reference, in the same file or another file
+- **When** When the embedded task is toggled
+- **Then** There is an error message saying that the task with the block reference could not be found, to be toggled
+  - See [Issue #688](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/688)
 
 ## The embedded tasks
 
-To reproduce issue 688, click on the following checkbox in Reading mode:
+---
 
 - ref task2 ![[#^ca47c7]]
+
+---
+
+- ref task4 ![[Embed File in to Note - File to Embed#^fromseparatefile]]
diff --git a/tests/EditorSuggest.test.ts b/tests/EditorSuggest.test.ts
deleted file mode 100644
index 0325a996..00000000
--- a/tests/EditorSuggest.test.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-import moment from 'moment';
-import { TASK_FORMATS, getSettings } from '../src/Config/Settings';
-import { buildSuggestions } from '../src/Suggestor/Suggestor';
-import type { SuggestInfo } from '../src/Suggestor/Suggestor';
-
-window.moment = moment;
-
-describe('auto-complete', () => {
-    it('offers basic completion options for an empty task', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] ';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 5, originalSettings);
-        expect(suggestions).toEqual([
-            { suggestionType: 'empty', displayText: '⏎', appendText: '\n' },
-            { displayText: '📅 due date', appendText: '📅 ' },
-            { displayText: '🛫 start date', appendText: '🛫 ' },
-            { displayText: '⏳ scheduled date', appendText: '⏳ ' },
-            { displayText: '⏫ high priority', appendText: '⏫ ' },
-            { displayText: '🔼 medium priority', appendText: '🔼 ' },
-        ]);
-    });
-
-    it('offers generic due date completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 📅';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
-        expect(suggestions[0].displayText).toContain('today');
-        expect(suggestions[1].displayText).toContain('tomorrow');
-        expect(suggestions.length).toEqual(6);
-    });
-
-    it('offers specific due date completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 📅 to';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 20, originalSettings);
-        expect(suggestions[0].displayText).toContain('today');
-        expect(suggestions[1].displayText).toContain('tomorrow');
-    });
-
-    it('offers generic recurrence completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 🔁';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
-        expect(suggestions[0].displayText).toEqual('every');
-        expect(suggestions[1].displayText).toEqual('every day');
-        expect(suggestions[2].displayText).toEqual('every week');
-    });
-
-    it('offers specific recurrence completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 🔁 every w';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 25, originalSettings);
-        expect(suggestions[0].displayText).toEqual('every week');
-        expect(suggestions[1].displayText).toEqual('every week on Sunday');
-        expect(suggestions[2].displayText).toEqual('every week on Monday');
-    });
-
-    it('respects the minimal match setting', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        originalSettings.autoSuggestMinMatch = 2;
-        let line = '- [ ] some task 🔁 e';
-        let suggestions: SuggestInfo[] = buildSuggestions(line, 19, originalSettings);
-        expect(suggestions.length).toEqual(0);
-        line = '- [ ] some task 🔁 ev';
-        suggestions = buildSuggestions(line, 20, originalSettings);
-        expect(suggestions[0].displayText).toEqual('every');
-        expect(suggestions[1].displayText).toEqual('every day');
-    });
-
-    // Test disabled until I can figure out:
-    // 1. how to set the date.
-    // 2. how to set maxGenericSuggestions in Suggestor.ts to higher than 5.
-    // See suggestions in https://github.com/obsidian-tasks-group/obsidian-tasks/issues/861#issuecomment-1180788860
-    it.skip('show all suggested text', () => {
-        // Arrange
-        const { dueDateSymbol, scheduledDateSymbol, startDateSymbol, recurrenceSymbol } =
-            TASK_FORMATS.tasksPluginEmoji.taskSerializer.symbols;
-        const originalSettings = getSettings();
-        originalSettings.autoSuggestMaxItems = 200;
-
-        // This does not change the date used in the suggestions below.
-        // It was a failed attempt at allowing this test to be independent of date of the run.
-        // const todaySpy = jest
-        //     .spyOn(Date, 'now')
-        //     .mockReturnValue(moment('2022-06-11').valueOf());
-
-        const lines = [
-            '- [ ] some task',
-            `- [ ] some task ${recurrenceSymbol} `,
-            `- [ ] some task ${dueDateSymbol} `,
-            `- [ ] some task ${scheduledDateSymbol} `,
-            `- [ ] some task ${startDateSymbol} `,
-        ];
-        const allSuggestions: string[] = [];
-        for (const line of lines) {
-            // allSuggestions.push(`| ${line} | |`);
-            const suggestions: SuggestInfo[] = buildSuggestions(line, line.length - 1, originalSettings);
-            for (const suggestion of suggestions) {
-                // The 'new line' replacement adds a trailing space at the end of a line,
-                // which causes auto-formatting to then make the test fail.
-                // So we replace the 'new line' character with some fixed text.
-                let replacementText = `${suggestion.appendText}`;
-                if (replacementText === '\n') {
-                    replacementText = '<new line>';
-                }
-                // Format for pasting in to a Markdown table:
-                const suggestionAsText = `| ${suggestion.displayText} | ${replacementText} |`;
-                if (!allSuggestions.includes(suggestionAsText)) {
-                    allSuggestions.push(suggestionAsText);
-                }
-            }
-        }
-
-        // CAUTION: In obsidian, when using this feature on a 'Monday' expands to the execution date.
-        // In these tests, it expands to the Next Monday.
-
-        expect('\n' + allSuggestions.join('\n') + '\n').toMatchInlineSnapshot(`
-            "
-            | ⏎ | <new line> |
-            | 📅 due date | 📅  |
-            | 🛫 start date | 🛫  |
-            | ⏳ scheduled date | ⏳  |
-            | ⏫ high priority | ⏫  |
-            | 🔼 medium priority | 🔼  |
-            | 🔽 low priority | 🔽  |
-            | 🔁 recurring (repeat) | 🔁  |
-            | every | 🔁 every  |
-            | every day | 🔁 every day  |
-            | every week | 🔁 every week  |
-            | every month | 🔁 every month  |
-            | every month on the | 🔁 every month on the  |
-            | every year | 🔁 every year  |
-            | every week on Sunday | 🔁 every week on Sunday  |
-            | every week on Monday | 🔁 every week on Monday  |
-            | every week on Tuesday | 🔁 every week on Tuesday  |
-            | every week on Wednesday | 🔁 every week on Wednesday  |
-            | every week on Thursday | 🔁 every week on Thursday  |
-            | every week on Friday | 🔁 every week on Friday  |
-            | every week on Saturday | 🔁 every week on Saturday  |
-            | today (2022-07-11) | 📅 2022-07-11  |
-            | tomorrow (2022-07-12) | 📅 2022-07-12  |
-            | Sunday (2022-07-17) | 📅 2022-07-17  |
-            | Monday (2022-07-18) | 📅 2022-07-18  |
-            | Tuesday (2022-07-12) | 📅 2022-07-12  |
-            | Wednesday (2022-07-13) | 📅 2022-07-13  |
-            | Thursday (2022-07-14) | 📅 2022-07-14  |
-            | Friday (2022-07-15) | 📅 2022-07-15  |
-            | Saturday (2022-07-16) | 📅 2022-07-16  |
-            | next week (2022-07-18) | 📅 2022-07-18  |
-            | next month (2022-08-11) | 📅 2022-08-11  |
-            | next year (2023-07-11) | 📅 2023-07-11  |
-            | today (2022-07-11) | ⏳ 2022-07-11  |
-            | tomorrow (2022-07-12) | ⏳ 2022-07-12  |
-            | Sunday (2022-07-17) | ⏳ 2022-07-17  |
-            | Monday (2022-07-18) | ⏳ 2022-07-18  |
-            | Tuesday (2022-07-12) | ⏳ 2022-07-12  |
-            | Wednesday (2022-07-13) | ⏳ 2022-07-13  |
-            | Thursday (2022-07-14) | ⏳ 2022-07-14  |
-            | Friday (2022-07-15) | ⏳ 2022-07-15  |
-            | Saturday (2022-07-16) | ⏳ 2022-07-16  |
-            | next week (2022-07-18) | ⏳ 2022-07-18  |
-            | next month (2022-08-11) | ⏳ 2022-08-11  |
-            | next year (2023-07-11) | ⏳ 2023-07-11  |
-            | today (2022-07-11) | 🛫 2022-07-11  |
-            | tomorrow (2022-07-12) | 🛫 2022-07-12  |
-            | Sunday (2022-07-17) | 🛫 2022-07-17  |
-            | Monday (2022-07-18) | 🛫 2022-07-18  |
-            | Tuesday (2022-07-12) | 🛫 2022-07-12  |
-            | Wednesday (2022-07-13) | 🛫 2022-07-13  |
-            | Thursday (2022-07-14) | 🛫 2022-07-14  |
-            | Friday (2022-07-15) | 🛫 2022-07-15  |
-            | Saturday (2022-07-16) | 🛫 2022-07-16  |
-            | next week (2022-07-18) | 🛫 2022-07-18  |
-            | next month (2022-08-11) | 🛫 2022-08-11  |
-            | next year (2023-07-11) | 🛫 2023-07-11  |
-            "
-        `);
-
-        // todaySpy.mockClear();
-    });
-});
diff --git a/tests/File.test.replaceTaskWithTasks_issue 1680 - Cannot read properties of undefined correct behaviour.approved.txt b/tests/File.test.replaceTaskWithTasks_issue 1680 - Cannot read properties of undefined correct behaviour.approved.txt
deleted file mode 100644
index 35f6fc08..00000000
--- a/tests/File.test.replaceTaskWithTasks_issue 1680 - Cannot read properties of undefined correct behaviour.approved.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-Could not find line for task
diff --git a/tests/File.test.replaceTaskWithTasks_issue 1680 - Cannot read properties of undefined current incorrect behaviour.approved.txt b/tests/File.test.replaceTaskWithTasks_issue 1680 - Cannot read properties of undefined current incorrect behaviour.approved.txt
deleted file mode 100644
index 35f6fc08..00000000
--- a/tests/File.test.replaceTaskWithTasks_issue 1680 - Cannot read properties of undefined current incorrect behaviour.approved.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-Could not find line for task
diff --git a/tests/File.test.replaceTaskWithTasks_issue 688 - block referenced task correct behaviour.approved.txt b/tests/File.test.replaceTaskWithTasks_issue 688 - block referenced task correct behaviour.approved.txt
deleted file mode 100644
index 99f9c454..00000000
--- a/tests/File.test.replaceTaskWithTasks_issue 688 - block referenced task correct behaviour.approved.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Tasks: Unable to find task in file Manual Testing/Task Toggling Scenarios/Embed Task in to Note.md.
-Expected task:
-- [ ] #task task2b ^ca47c7
-Found task:
-- [ ] #task task1a
diff --git a/tests/File.test.replaceTaskWithTasks_issue 688 - block referenced task current incorrect behaviour.approved.txt b/tests/File.test.replaceTaskWithTasks_issue 688 - block referenced task current incorrect behaviour.approved.txt
deleted file mode 100644
index 99f9c454..00000000
--- a/tests/File.test.replaceTaskWithTasks_issue 688 - block referenced task current incorrect behaviour.approved.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Tasks: Unable to find task in file Manual Testing/Task Toggling Scenarios/Embed Task in to Note.md.
-Expected task:
-- [ ] #task task2b ^ca47c7
-Found task:
-- [ ] #task task1a
diff --git a/tests/File.test.replaceTaskWithTasks_valid 2-task test.approved.txt b/tests/File.test.replaceTaskWithTasks_valid 2-task test.approved.txt
deleted file mode 100644
index bd7d320e..00000000
--- a/tests/File.test.replaceTaskWithTasks_valid 2-task test.approved.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-Success. Line found OK. No error reported.
diff --git a/tests/File.test.ts b/tests/File.test.ts
deleted file mode 100644
index 25f2ddea..00000000
--- a/tests/File.test.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { readFileSync } from 'fs';
-import { verify } from 'approvals/lib/Providers/Jest/JestApprovals';
-import { findLineNumberOfTaskToToggle } from '../src/File';
-import type { MockTogglingDataForTesting } from '../src/lib/MockDataCreator';
-
-/**
- * A function to help test File.findLineNumberOfTaskToToggle()
- *
- * It uses mock data to simulate user behaviour.
- *
- * This is a fragile method of testing, but right now is better than no automated
- * testing at all.
- *
- * Input files for {@link jsonFileName} are in `tests/__test_data__/MockDataForTogglingTasks`
- * They are created by:
- *   1. setting logDataForMocking to true in File.ts.
- *   2. Running a local build on the plugin
- *   3. Showing the console
- *   4. Setting up a small markdown file with the scenario to test
- *   5. Working the steps required to make the toggle operation fail
- *   6. Grabbing the console output
- *   7. Pasting it in to a new JSON file in `tests/__test_data__/MockDataForTogglingTasks`
- *   8. Auto-formatting the JSON so that it is readable.
- * @param jsonFileName
- * @param taskLineToToggle
- * @param expectedLineNumber
- * @param actualIncorrectLineFound - optional, only needed if the wrong line would be toggled.
- */
-function testFindLineNumberOfTaskToToggle(
-    jsonFileName: string,
-    taskLineToToggle: string,
-    expectedLineNumber: number | undefined,
-    actualIncorrectLineFound?: string,
-) {
-    // Arrange
-    const data = readFileSync('tests/__test_data__/MockDataForTogglingTasks/' + jsonFileName, 'utf-8');
-    const everything: MockTogglingDataForTesting = JSON.parse(data);
-    expect(everything.taskData.originalMarkdown).toEqual(taskLineToToggle);
-
-    // Act
-    const originalTask = everything.taskData;
-    const fileLines = everything.fileData.fileLines;
-    const listItemsCache = everything.cacheData.listItemsCache;
-
-    let errorString: string | undefined;
-    const captureError = (message: string) => {
-        errorString = message;
-    };
-
-    const result = findLineNumberOfTaskToToggle(
-        originalTask,
-        fileLines,
-        listItemsCache,
-        captureError.bind(errorString),
-    );
-
-    // Assert
-    let descriptionOfOutcome = '';
-    if (errorString !== undefined) {
-        descriptionOfOutcome = errorString;
-    } else if (result === undefined) {
-        descriptionOfOutcome = 'Could not find line for task';
-    } else {
-        descriptionOfOutcome = 'Success. Line found OK. No error reported.';
-    }
-    verify(descriptionOfOutcome);
-
-    if (expectedLineNumber !== undefined) {
-        expect(result).not.toBeUndefined();
-        expect(result).toEqual(expectedLineNumber);
-
-        const expectedLine = actualIncorrectLineFound ? actualIncorrectLineFound : everything.taskData.originalMarkdown;
-        expect(everything.fileData.fileLines[result!]).toEqual(expectedLine);
-    } else {
-        expect(result).toBeUndefined();
-    }
-}
-
-describe('replaceTaskWithTasks', () => {
-    it('valid 2-task test', () => {
-        const jsonFileName = 'single_task_valid_data.json';
-        const taskLineToToggle = '- [ ] #task task 2';
-        const expectedLineNumber = 5;
-        testFindLineNumberOfTaskToToggle(jsonFileName, taskLineToToggle, expectedLineNumber);
-    });
-
-    // --------------------------------------------------------------------------------
-    // Issue 688
-    describe('issue 688 - block referenced task', () => {
-        const jsonFileName = '688_toggle_block_referenced_line_overwrites_wrong_line.json';
-        const taskLineToToggle = '- [ ] #task task2b ^ca47c7';
-
-        it.failing('correct behaviour', () => {
-            // An incorrect line is currently found, so this test fails, due to bug 688
-            const expectedLineNumber = 8;
-            testFindLineNumberOfTaskToToggle(jsonFileName, taskLineToToggle, expectedLineNumber);
-        });
-
-        it('current incorrect behaviour', () => {
-            // An incorrect line is currently found, due to bug 688.
-            // It is recognised as an incorrect line, and so line number is returned as undefined.
-            const expectedLineNumber = undefined;
-            // const actualIncorrectLineFound = '- [ ] #task task1a';
-            testFindLineNumberOfTaskToToggle(jsonFileName, taskLineToToggle, expectedLineNumber);
-        });
-    });
-
-    // --------------------------------------------------------------------------------
-    // Issue 1680
-    describe('issue 1680 - Cannot read properties of undefined', () => {
-        const jsonFileName = '1680_task_line_number_past_end_of_file.json';
-        const taskLineToToggle = '- [ ] #task Section 2/Task 2';
-
-        it.failing('correct behaviour', () => {
-            // An incorrect line is currently found, so this test fails, due to bug 1680
-            const expectedLineNumber = 9;
-            testFindLineNumberOfTaskToToggle(jsonFileName, taskLineToToggle, expectedLineNumber);
-        });
-
-        it('current incorrect behaviour', () => {
-            // An incorrect line is currently found, due to bug 688.
-            // It is recognised as an incorrect line, and so line number is returned as undefined.
-            const expectedLineNumber = undefined;
-            testFindLineNumberOfTaskToToggle(jsonFileName, taskLineToToggle, expectedLineNumber);
-        });
-    });
-
-    // --------------------------------------------------------------------------------
-});
diff --git a/tests/Suggestor/Suggestor.test.ts b/tests/Suggestor/Suggestor.test.ts
new file mode 100644
index 00000000..76c62301
--- /dev/null
+++ b/tests/Suggestor/Suggestor.test.ts
@@ -0,0 +1,191 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { getSettings } from '../../src/Config/Settings';
+import type { SuggestInfo } from '../../src/Suggestor';
+import { makeDefaultSuggestionBuilder } from '../../src/Suggestor/Suggestor';
+import { DEFAULT_SYMBOLS } from '../../src/TaskSerializer/DefaultTaskSerializer';
+
+window.moment = moment;
+
+describe.each([{ name: 'emoji', symbols: DEFAULT_SYMBOLS }])("auto-complete with '$name' symbols", ({ symbols }) => {
+    const buildSuggestions = makeDefaultSuggestionBuilder(symbols);
+    const { dueDateSymbol, scheduledDateSymbol, startDateSymbol, recurrenceSymbol, prioritySymbols } = symbols;
+    it('offers basic completion options for an empty task', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = '- [ ] ';
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 5, originalSettings);
+        expect(suggestions).toEqual([
+            { suggestionType: 'empty', displayText: '⏎', appendText: '\n' },
+            { displayText: `${dueDateSymbol} due date`, appendText: `${dueDateSymbol} ` },
+            { displayText: `${startDateSymbol} start date`, appendText: `${startDateSymbol} ` },
+            { displayText: `${scheduledDateSymbol} scheduled date`, appendText: `${scheduledDateSymbol} ` },
+            { displayText: `${prioritySymbols.High} high priority`, appendText: `${prioritySymbols.High} ` },
+            { displayText: `${prioritySymbols.Medium} medium priority`, appendText: `${prioritySymbols.Medium} ` },
+        ]);
+    });
+
+    it('offers generic due date completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${dueDateSymbol}`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
+        expect(suggestions[0].displayText).toContain('today');
+        expect(suggestions[1].displayText).toContain('tomorrow');
+        expect(suggestions.length).toEqual(6);
+    });
+
+    it('offers specific due date completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${dueDateSymbol} to`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 20, originalSettings);
+        expect(suggestions[0].displayText).toContain('today');
+        expect(suggestions[1].displayText).toContain('tomorrow');
+    });
+
+    it('offers generic recurrence completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${recurrenceSymbol}`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
+        expect(suggestions[0].displayText).toEqual('every');
+        expect(suggestions[1].displayText).toEqual('every day');
+        expect(suggestions[2].displayText).toEqual('every week');
+    });
+
+    it('offers specific recurrence completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${recurrenceSymbol} every w`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 25, originalSettings);
+        expect(suggestions[0].displayText).toEqual('every week');
+        expect(suggestions[1].displayText).toEqual('every week on Sunday');
+        expect(suggestions[2].displayText).toEqual('every week on Monday');
+    });
+
+    it('respects the minimal match setting', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        originalSettings.autoSuggestMinMatch = 2;
+        let line = `- [ ] some task ${recurrenceSymbol} e`;
+        let suggestions: SuggestInfo[] = buildSuggestions(line, 19, originalSettings);
+        expect(suggestions.length).toEqual(0);
+        line = `- [ ] some task ${recurrenceSymbol} ev`;
+        suggestions = buildSuggestions(line, 20, originalSettings);
+        expect(suggestions[0].displayText).toEqual('every');
+        expect(suggestions[1].displayText).toEqual('every day');
+    });
+
+    // Test disabled until I can figure out:
+    // 1. how to set the date.
+    // 2. how to set maxGenericSuggestions in Suggestor.ts to higher than 5.
+    // See suggestions in https://github.com/obsidian-tasks-group/obsidian-tasks/issues/861#issuecomment-1180788860
+    it.skip('show all suggested text', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        originalSettings.autoSuggestMaxItems = 200;
+
+        // This does not change the date used in the suggestions below.
+        // It was a failed attempt at allowing this test to be independent of date of the run.
+        // const todaySpy = jest
+        //     .spyOn(Date, 'now')
+        //     .mockReturnValue(moment('2022-06-11').valueOf());
+
+        const lines = [
+            '- [ ] some task',
+            `- [ ] some task ${recurrenceSymbol} `,
+            `- [ ] some task ${dueDateSymbol} `,
+            `- [ ] some task ${scheduledDateSymbol} `,
+            `- [ ] some task ${startDateSymbol} `,
+        ];
+        const allSuggestions: string[] = [];
+        for (const line of lines) {
+            // allSuggestions.push(`| ${line} | |`);
+            const suggestions: SuggestInfo[] = buildSuggestions(line, line.length - 1, originalSettings);
+            for (const suggestion of suggestions) {
+                // The 'new line' replacement adds a trailing space at the end of a line,
+                // which causes auto-formatting to then make the test fail.
+                // So we replace the 'new line' character with some fixed text.
+                let replacementText = `${suggestion.appendText}`;
+                if (replacementText === '\n') {
+                    replacementText = '<new line>';
+                }
+                // Format for pasting in to a Markdown table:
+                const suggestionAsText = `| ${suggestion.displayText} | ${replacementText} |`;
+                if (!allSuggestions.includes(suggestionAsText)) {
+                    allSuggestions.push(suggestionAsText);
+                }
+            }
+        }
+
+        // CAUTION: In obsidian, when using this feature on a 'Monday' expands to the execution date.
+        // In these tests, it expands to the Next Monday.
+
+        expect('\n' + allSuggestions.join('\n') + '\n').toMatchInlineSnapshot(`
+            "
+            | ⏎ | <new line> |
+            | ${dueDateSymbol} due date | ${dueDateSymbol}  |
+            | ${startDateSymbol} start date | ${startDateSymbol}  |
+            | ${scheduledDateSymbol} scheduled date | ${scheduledDateSymbol}  |
+            | ${prioritySymbols.High} high priority | ${prioritySymbols.High}  |
+            | ${prioritySymbols.Medium} medium priority | ${prioritySymbols.Medium}  |
+            | ${prioritySymbols.Low} low priority | ${prioritySymbols.Low}  |
+            | ${recurrenceSymbol} recurring (repeat) | ${recurrenceSymbol}  |
+            | every | ${recurrenceSymbol} every  |
+            | every day | ${recurrenceSymbol} every day  |
+            | every week | ${recurrenceSymbol} every week  |
+            | every month | ${recurrenceSymbol} every month  |
+            | every month on the | ${recurrenceSymbol} every month on the  |
+            | every year | ${recurrenceSymbol} every year  |
+            | every week on Sunday | ${recurrenceSymbol} every week on Sunday  |
+            | every week on Monday | ${recurrenceSymbol} every week on Monday  |
+            | every week on Tuesday | ${recurrenceSymbol} every week on Tuesday  |
+            | every week on Wednesday | ${recurrenceSymbol} every week on Wednesday  |
+            | every week on Thursday | ${recurrenceSymbol} every week on Thursday  |
+            | every week on Friday | ${recurrenceSymbol} every week on Friday  |
+            | every week on Saturday | ${recurrenceSymbol} every week on Saturday  |
+            | today (2022-07-11) | ${dueDateSymbol} 2022-07-11  |
+            | tomorrow (2022-07-12) | ${dueDateSymbol} 2022-07-12  |
+            | Sunday (2022-07-17) | ${dueDateSymbol} 2022-07-17  |
+            | Monday (2022-07-18) | ${dueDateSymbol} 2022-07-18  |
+            | Tuesday (2022-07-12) | ${dueDateSymbol} 2022-07-12  |
+            | Wednesday (2022-07-13) | ${dueDateSymbol} 2022-07-13  |
+            | Thursday (2022-07-14) | ${dueDateSymbol} 2022-07-14  |
+            | Friday (2022-07-15) | ${dueDateSymbol} 2022-07-15  |
+            | Saturday (2022-07-16) | ${dueDateSymbol} 2022-07-16  |
+            | next week (2022-07-18) | ${dueDateSymbol} 2022-07-18  |
+            | next month (2022-08-11) | ${dueDateSymbol} 2022-08-11  |
+            | next year (2023-07-11) | ${dueDateSymbol} 2023-07-11  |
+            | today (2022-07-11) | ${scheduledDateSymbol} 2022-07-11  |
+            | tomorrow (2022-07-12) | ${scheduledDateSymbol} 2022-07-12  |
+            | Sunday (2022-07-17) | ${scheduledDateSymbol} 2022-07-17  |
+            | Monday (2022-07-18) | ${scheduledDateSymbol} 2022-07-18  |
+            | Tuesday (2022-07-12) | ${scheduledDateSymbol} 2022-07-12  |
+            | Wednesday (2022-07-13) | ${scheduledDateSymbol} 2022-07-13  |
+            | Thursday (2022-07-14) | ${scheduledDateSymbol} 2022-07-14  |
+            | Friday (2022-07-15) | ${scheduledDateSymbol} 2022-07-15  |
+            | Saturday (2022-07-16) | ${scheduledDateSymbol} 2022-07-16  |
+            | next week (2022-07-18) | ${scheduledDateSymbol} 2022-07-18  |
+            | next month (2022-08-11) | ${scheduledDateSymbol} 2022-08-11  |
+            | next year (2023-07-11) | ${scheduledDateSymbol} 2023-07-11  |
+            | today (2022-07-11) | ${startDateSymbol} 2022-07-11  |
+            | tomorrow (2022-07-12) | ${startDateSymbol} 2022-07-12  |
+            | Sunday (2022-07-17) | ${startDateSymbol} 2022-07-17  |
+            | Monday (2022-07-18) | ${startDateSymbol} 2022-07-18  |
+            | Tuesday (2022-07-12) | ${startDateSymbol} 2022-07-12  |
+            | Wednesday (2022-07-13) | ${startDateSymbol} 2022-07-13  |
+            | Thursday (2022-07-14) | ${startDateSymbol} 2022-07-14  |
+            | Friday (2022-07-15) | ${startDateSymbol} 2022-07-15  |
+            | Saturday (2022-07-16) | ${startDateSymbol} 2022-07-16  |
+            | next week (2022-07-18) | ${startDateSymbol} 2022-07-18  |
+            | next month (2022-08-11) | ${startDateSymbol} 2022-08-11  |
+            | next year (2023-07-11) | ${startDateSymbol} 2023-07-11  |
+            "
+        `);
+
+        // todaySpy.mockClear();
+    });
+});
diff --git a/tests/__test_data__/MockDataForTogglingTasks/1680_task_line_number_past_end_of_file.json b/tests/__test_data__/MockDataForTogglingTasks/1680_task_line_number_past_end_of_file.json
deleted file mode 100644
index 08e09208..00000000
--- a/tests/__test_data__/MockDataForTogglingTasks/1680_task_line_number_past_end_of_file.json
+++ /dev/null
@@ -1,91 +0,0 @@
-{
-  "taskData": {
-    "originalMarkdown": "- [ ] #task Section 2/Task 2",
-    "taskLocation": {
-      "path": "Manual Testing/Task Toggling Scenarios/1680 - Reading Mode line numbers not updated on editing.md",
-      "lineNumber": 14,
-      "sectionStart": 13,
-      "sectionIndex": 1,
-      "precedingHeader": null
-    }
-  },
-  "fileData": {
-    "fileLines": [
-      "## Test Data",
-      "",
-      "### Tasks Section 1",
-      "- [ ] #task Section 1/Task 1",
-      "- [ ] #task Section 1/Task 2",
-      "",
-      "### Tasks Section 2",
-      "",
-      "- [ ] #task Section 2/Task 1",
-      "- [ ] #task Section 2/Task 2",
-      ""
-    ]
-  },
-  "cacheData": {
-    "listItemsCache": [
-      {
-        "position": {
-          "start": {
-            "line": 8,
-            "col": 0,
-            "offset": 39
-          },
-          "end": {
-            "line": 8,
-            "col": 28,
-            "offset": 67
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 9,
-            "col": 0,
-            "offset": 68
-          },
-          "end": {
-            "line": 9,
-            "col": 28,
-            "offset": 96
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 13,
-            "col": 0,
-            "offset": 119
-          },
-          "end": {
-            "line": 13,
-            "col": 28,
-            "offset": 147
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 14,
-            "col": 0,
-            "offset": 148
-          },
-          "end": {
-            "line": 14,
-            "col": 28,
-            "offset": 176
-          }
-        },
-        "task": " "
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/__test_data__/MockDataForTogglingTasks/688_toggle_block_referenced_line_overwrites_wrong_line.json b/tests/__test_data__/MockDataForTogglingTasks/688_toggle_block_referenced_line_overwrites_wrong_line.json
deleted file mode 100644
index c68ed0f4..00000000
--- a/tests/__test_data__/MockDataForTogglingTasks/688_toggle_block_referenced_line_overwrites_wrong_line.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
-  "taskData": {
-    "originalMarkdown": "- [ ] #task task2b ^ca47c7",
-    "taskLocation": {
-      "path": "Manual Testing/Task Toggling Scenarios/Embed Task in to Note.md",
-      "lineNumber": 0,
-      "sectionStart": 0,
-      "sectionIndex": 0,
-      "precedingHeader": null
-    }
-  },
-  "fileData": {
-    "fileLines": [
-      "## Category 1",
-      "",
-      "- [ ] #task task1a",
-      "- [ ] #task task1b",
-      "",
-      "## Category 2",
-      "",
-      "- [ ] #task task2a",
-      "- [ ] #task task2b ^ca47c7",
-      "",
-      "- ref task2 ![[#^ca47c7]]",
-      ""
-    ]
-  },
-  "cacheData": {
-    "listItemsCache": [
-      {
-        "position": {
-          "start": {
-            "line": 2,
-            "col": 0,
-            "offset": 15
-          },
-          "end": {
-            "line": 2,
-            "col": 18,
-            "offset": 33
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 3,
-            "col": 0,
-            "offset": 34
-          },
-          "end": {
-            "line": 3,
-            "col": 18,
-            "offset": 52
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 7,
-            "col": 0,
-            "offset": 69
-          },
-          "end": {
-            "line": 7,
-            "col": 18,
-            "offset": 87
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 8,
-            "col": 0,
-            "offset": 88
-          },
-          "end": {
-            "line": 9,
-            "col": 0,
-            "offset": 115
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 10,
-            "col": 0,
-            "offset": 116
-          },
-          "end": {
-            "line": 10,
-            "col": 25,
-            "offset": 141
-          }
-        }
-      }
-    ]
-  }
-}
\ No newline at end of file
diff --git a/tests/__test_data__/MockDataForTogglingTasks/single_task_valid_data.json b/tests/__test_data__/MockDataForTogglingTasks/single_task_valid_data.json
deleted file mode 100644
index 1b9c3ac4..00000000
--- a/tests/__test_data__/MockDataForTogglingTasks/single_task_valid_data.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
-  "taskData": {
-    "originalMarkdown": "- [ ] #task task 2",
-    "taskLocation": {
-      "path": "Manual Testing/Task Toggling Scenarios/2 Tasks.md",
-      "lineNumber": 5,
-      "sectionStart": 4,
-      "sectionIndex": 1,
-      "precedingHeader": null
-    }
-  },
-  "fileData": {
-    "fileLines": [
-      "# Toggle 2nd of two tasks",
-      "",
-      "## Heading 2",
-      "",
-      "- [ ] #task task 1",
-      "- [ ] #task task 2",
-      ""
-    ]
-  },
-  "cacheData": {
-    "listItemsCache": [
-      {
-        "position": {
-          "start": {
-            "line": 4,
-            "col": 0,
-            "offset": 41
-          },
-          "end": {
-            "line": 4,
-            "col": 18,
-            "offset": 59
-          }
-        },
-        "task": " "
-      },
-      {
-        "position": {
-          "start": {
-            "line": 5,
-            "col": 0,
-            "offset": 60
-          },
-          "end": {
-            "line": 5,
-            "col": 18,
-            "offset": 78
-          }
-        },
-        "task": " "
-      }
-    ]
-  }
-}
\ No newline at end of file