Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2826 (2024-05-15 06:23:26)

fix: Test and improve behaviour of auto-suggest with dependencies
<!--- Provide a general summary of your changes in the Title above -->

# Description

I'm using the new auto-suggest for ID and dependsOn in my own vault, and it's incredibly valuable.

Before release, I wanted to add tests to make sure I understand the code.

Along the way, I improved a couple of behaviours:

- 99129ea218f5b4771259aa5e3b66663fa18cf6ee - allow additional dependencies after ids with hyphen and underscore
- bcee94a9e5eaa32a3dd2068de56824bb2878c3d2 - correct the search for blocking tasks to use exact id match, not substring

## Motivation and Context

Getting this facility ready for release

## How has this been tested?

- Adding new tests
- Manual exploratory testing

## 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:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)

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)

## 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.
- [ ] 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 follows 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/Scripting/Postponer.ts b/src/Scripting/Postponer.ts
index c3674e99..06aa55b6 100644
--- a/src/Scripting/Postponer.ts
+++ b/src/Scripting/Postponer.ts
@@ -1,5 +1,4 @@
 import type { Moment, unitOfTime } from 'moment';
-import { capitalizeFirstLetter } from '../lib/StringHelpers';
 import { DateFallback } from '../Task/DateFallback';
 import { Task } from '../Task/Task';
 import { TasksDate } from './TasksDate';
@@ -112,6 +111,10 @@ export function postponeButtonTitle(task: Task, amount: number, timeUnit: unitOf
     return `ℹ️ ${buttonText} (right-click for more options)`;
 }
 
+function capitalizeFirstLetter(word: string) {
+    return word.charAt(0).toUpperCase() + word.slice(1);
+}
+
 /**
  * Get the menu text to use when changing a task date relative to its current value.
  * @param task
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index d9f2b862..82a3ce52 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -3,7 +3,7 @@ import type { Settings } from '../Config/Settings';
 import { DateParser } from '../Query/DateParser';
 import { doAutocomplete } from '../lib/DateAbbreviations';
 import { Recurrence } from '../Task/Recurrence';
-import type { DefaultTaskSerializerSymbols } from '../TaskSerializer/DefaultTaskSerializer';
+import { type DefaultTaskSerializerSymbols, taskIdRegex } from '../TaskSerializer/DefaultTaskSerializer';
 import { Task } from '../Task/Task';
 import { generateUniqueId } from '../Task/TaskDependency';
 import { GlobalFilter } from '../Config/GlobalFilter';
@@ -410,7 +410,7 @@ function addRecurrenceSuggestions(
 
 function addIDSuggestion(line: string, cursorPos: number, idSymbol: string, allTasks: Task[]) {
     const results: SuggestInfo[] = [];
-    const idRegex = new RegExp(`(${idSymbol})\\s*([0-9a-zA-Z ]*)`, 'ug');
+    const idRegex = new RegExp(`(${idSymbol})\\s*(${taskIdRegex.source})?`, 'ug');
     const idMatch = matchIfCursorInRegex(line, idRegex, cursorPos);
 
     if (idMatch && idMatch[0].trim().length <= idSymbol.length) {
@@ -453,7 +453,11 @@ function addDependsOnSuggestions(
         // Find all Tasks, Already Added
         let blockingTasks: Task[] = [];
         if (existingDependsOnIdStrings) {
-            blockingTasks = allTasks.filter((task) => task.id && existingDependsOnIdStrings.includes(task.id));
+            // Split the string into an array by commas, then map over it to trim whitespace from each element.
+            const idsArray = existingDependsOnIdStrings.split(',').map((id) => id.trim());
+
+            // Filter `allTasks` to only include tasks whose `id` is exactly in the `idsArray`.
+            blockingTasks = allTasks.filter((task) => task.id && idsArray.includes(task.id));
         }
 
         if (newTaskToAppend.length >= settings.autoSuggestMinMatch) {
diff --git a/src/TaskSerializer/DataviewTaskSerializer.ts b/src/TaskSerializer/DataviewTaskSerializer.ts
index 35945d8e..ed60a386 100644
--- a/src/TaskSerializer/DataviewTaskSerializer.ts
+++ b/src/TaskSerializer/DataviewTaskSerializer.ts
@@ -1,7 +1,7 @@
 import { TaskLayoutComponent } from '../Layout/TaskLayoutOptions';
 import { Priority } from '../Task/Priority';
 import type { Task } from '../Task/Task';
-import { DefaultTaskSerializer, taskIdRegex } from './DefaultTaskSerializer';
+import { DefaultTaskSerializer, taskIdRegex, taskIdSequenceRegex } from './DefaultTaskSerializer';
 
 /**
  * Takes a regex of the form 'key:: value' and turns it into a regex that can parse
@@ -86,9 +86,7 @@ export const DATAVIEW_SYMBOLS = {
         doneDateRegex: toInlineFieldRegex(/completion:: *(\d{4}-\d{2}-\d{2})/),
         cancelledDateRegex: toInlineFieldRegex(/cancelled:: *(\d{4}-\d{2}-\d{2})/),
         recurrenceRegex: toInlineFieldRegex(/repeat:: *([a-zA-Z0-9, !]+)/),
-        dependsOnRegex: toInlineFieldRegex(
-            new RegExp('dependsOn:: *(' + taskIdRegex.source + '( *, *' + taskIdRegex.source + ' *)*)'),
-        ),
+        dependsOnRegex: toInlineFieldRegex(new RegExp('dependsOn:: *(' + taskIdSequenceRegex.source + ')')),
         idRegex: toInlineFieldRegex(new RegExp('id:: *(' + taskIdRegex.source + ')')),
     },
 } as const;
diff --git a/src/TaskSerializer/DefaultTaskSerializer.ts b/src/TaskSerializer/DefaultTaskSerializer.ts
index b8e3e016..46e9361c 100644
--- a/src/TaskSerializer/DefaultTaskSerializer.ts
+++ b/src/TaskSerializer/DefaultTaskSerializer.ts
@@ -48,6 +48,9 @@ export interface DefaultTaskSerializerSymbols {
 // The allowed characters in a single task id:
 export const taskIdRegex = /[a-zA-Z0-9-_]+/;
 
+// The allowed characters in a comma-separated sequence of task ids:
+export const taskIdSequenceRegex = new RegExp(taskIdRegex.source + '( *, *' + taskIdRegex.source + ' *)*');
+
 /**
  * A symbol map for obsidian-task's default task style.
  * Uses emojis to concisely convey meaning
@@ -83,10 +86,7 @@ export const DEFAULT_SYMBOLS: DefaultTaskSerializerSymbols = {
         doneDateRegex: /✅ *(\d{4}-\d{2}-\d{2})$/u,
         cancelledDateRegex: /❌ *(\d{4}-\d{2}-\d{2})$/u,
         recurrenceRegex: /🔁 ?([a-zA-Z0-9, !]+)$/iu,
-        dependsOnRegex: new RegExp(
-            '⛔\uFE0F? *(' + taskIdRegex.source + '( *, *' + taskIdRegex.source + ' *)*)$',
-            'iu',
-        ),
+        dependsOnRegex: new RegExp('⛔\uFE0F? *(' + taskIdSequenceRegex.source + ')$', 'iu'),
         idRegex: new RegExp('🆔 *(' + taskIdRegex.source + ')$', 'iu'),
     },
 } as const;
diff --git a/src/lib/StringHelpers.ts b/src/lib/StringHelpers.ts
deleted file mode 100644
index 396dd203..00000000
--- a/src/lib/StringHelpers.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function capitalizeFirstLetter(text: string): string {
-    return text.charAt(0).toUpperCase() + text.slice(1);
-}
diff --git a/src/ui/DateEditor.svelte b/src/ui/DateEditor.svelte
index 0f1472fb..8625e04e 100644
--- a/src/ui/DateEditor.svelte
+++ b/src/ui/DateEditor.svelte
@@ -1,7 +1,6 @@
 <script lang="ts">
     import { doAutocomplete } from '../lib/DateAbbreviations';
     import { parseTypedDateForDisplayUsingFutureDate } from '../lib/DateTools';
-    import { labelContentWithAccessKey } from './EditTaskHelpers';
 
     export let id: 'start' | 'scheduled' | 'due' | 'done' | 'created' | 'cancelled';
     export let dateSymbol: string;
@@ -21,7 +20,6 @@
     const datePlaceholder = "Try 'Mon' or 'tm' then space";
 </script>
 
-<label for={id}>{@html labelContentWithAccessKey(id, accesskey)}</label>
 <!-- svelte-ignore a11y-accesskey -->
 <input
     bind:value={date}
diff --git a/src/ui/Dependency.svelte b/src/ui/Dependency.svelte
index 83a18c4e..89314fd4 100644
--- a/src/ui/Dependency.svelte
+++ b/src/ui/Dependency.svelte
@@ -3,15 +3,14 @@
     import type { Task } from '../Task/Task';
     import type { EditableTask } from './EditableTask';
     import { descriptionAdjustedForDependencySearch, searchForCandidateTasksForDependency } from './DependencyHelpers';
-    import { labelContentWithAccessKey } from './EditTaskHelpers';
 
     export let task: Task;
     export let editableTask: EditableTask;
     export let allTasks: Task[];
     export let _onDescriptionKeyDown: (e: KeyboardEvent) => void;
     export let type: 'blocking' | 'blockedBy';
-    export let labelText: string;
-    export let accesskey: string | null;
+    export let accesskey: (key: string) => string | null;
+    export let accesskeyLetter: string = '';
     export let placeholder: string = 'Type to search...';
 
     let search: string = '';
@@ -142,7 +141,6 @@
     }
 </script>
 
-<label for={type}>{@html labelContentWithAccessKey(labelText, accesskey)}</label>
 <!-- svelte-ignore a11y-accesskey -->
 <span bind:clientWidth={inputWidth}>
     <input
@@ -151,7 +149,7 @@
         on:keydown={(e) => taskKeydown(e)}
         on:focus={onFocused}
         on:blur={() => (inputFocused = false)}
-        {accesskey}
+        accesskey={accesskey(accesskeyLetter)}
         id={type}
         class="tasks-modal-dependency-input"
         type="text"
diff --git a/src/ui/EditTask.scss b/src/ui/EditTask.scss
index b020c5ff..fdeb81d7 100644
--- a/src/ui/EditTask.scss
+++ b/src/ui/EditTask.scss
@@ -11,8 +11,10 @@
     .tasks-modal-error {
         border: 1px solid red !important;
     }
+}
 
-    .accesskey {
+.with-accesskeys {
+    .accesskey-first::first-letter, .accesskey {
         text-decoration: underline;
         text-underline-offset: 1pt;
     }
diff --git a/src/ui/EditTask.svelte b/src/ui/EditTask.svelte
index 9fda5906..f3c52e0c 100644
--- a/src/ui/EditTask.svelte
+++ b/src/ui/EditTask.svelte
@@ -13,7 +13,6 @@
     import DateEditor from './DateEditor.svelte';
     import type { EditableTask } from './EditableTask';
     import Dependency from './Dependency.svelte';
-    import { labelContentWithAccessKey } from './EditTaskHelpers';
 
     // These exported variables are passed in as props by TaskModal.onOpen():
     export let task: Task;
@@ -421,14 +420,14 @@ Availability of access keys:
 - -: Cancelled
 -->
 
-<form class="tasks-modal" on:submit|preventDefault={_onSubmit}>
+<form class="tasks-modal" class:with-accesskeys={withAccessKeys} on:submit|preventDefault={_onSubmit}>
     <!-- NEW_TASK_FIELD_EDIT_REQUIRED -->
 
     <!-- --------------------------------------------------------------------------- -->
     <!--  Description  -->
     <!-- --------------------------------------------------------------------------- -->
     <section class="tasks-modal-description-section">
-        <label for="description">{@html labelContentWithAccessKey('Description', accesskey('t'))}</label>
+        <label for="description">Descrip<span class="accesskey">t</span>ion</label>
         <!-- svelte-ignore a11y-accesskey -->
         <textarea
             bind:value={editableTask.description}
@@ -459,16 +458,9 @@ Availability of access keys:
                     accesskey={accesskey(accessKey)}
                 />
                 <label for="priority-{value}">
-                    <!-- These is no need to extract this behaviour to something like labelContentWithAccessKey(),
-                    since this whole section will just go in a separate Svelte component and
-                    will not be reused elsewhere like labelContentWithAccessKey(). -->
-                    {#if withAccessKeys}
-                        <span>{label.substring(0, accessKeyIndex)}</span><span class="accesskey"
-                            >{label.substring(accessKeyIndex, accessKeyIndex + 1)}</span
-                        ><span>{label.substring(accessKeyIndex + 1)}</span>
-                    {:else}
-                        <span>{label}</span>
-                    {/if}
+                    <span>{label.substring(0, accessKeyIndex)}</span><span class="accesskey"
+                        >{label.substring(accessKeyIndex, accessKeyIndex + 1)}</span
+                    ><span>{label.substring(accessKeyIndex + 1)}</span>
                     {#if symbol && symbol.charCodeAt(0) >= 0x100}
                         <span>{symbol}</span>
                     {/if}
@@ -485,7 +477,7 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Recurrence  -->
         <!-- --------------------------------------------------------------------------- -->
-        <label for="recurrence">{@html labelContentWithAccessKey('Recurs', accesskey('r'))}</label>
+        <label for="recurrence" class="accesskey-first">Recurs</label>
         <!-- svelte-ignore a11y-accesskey -->
         <input
             bind:value={editableTask.recurrenceRule}
@@ -500,6 +492,7 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Due Date  -->
         <!-- --------------------------------------------------------------------------- -->
+        <label for="due" class="accesskey-first">Due</label>
         <DateEditor
             id="due"
             dateSymbol={dueDateSymbol}
@@ -512,6 +505,7 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Scheduled Date  -->
         <!-- --------------------------------------------------------------------------- -->
+        <label for="scheduled" class="accesskey-first">Scheduled</label>
         <DateEditor
             id="scheduled"
             dateSymbol={scheduledDateSymbol}
@@ -524,6 +518,7 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Start Date  -->
         <!-- --------------------------------------------------------------------------- -->
+        <label for="start">St<span class="accesskey">a</span>rt</label>
         <DateEditor
             id="start"
             dateSymbol={startDateSymbol}
@@ -537,7 +532,10 @@ Availability of access keys:
         <!--  Only future dates  -->
         <!-- --------------------------------------------------------------------------- -->
         <div class="future-dates-only">
-            <label for="forwardOnly">{@html labelContentWithAccessKey('Only future dates:', accesskey('f'))}</label>
+            <label for="forwardOnly"
+                >Only
+                <span class="accesskey-first">future</span> dates:</label
+            >
             <!-- svelte-ignore a11y-accesskey -->
             <input
                 bind:checked={editableTask.forwardOnly}
@@ -558,28 +556,30 @@ Availability of access keys:
             <!-- --------------------------------------------------------------------------- -->
             <!--  Blocked By Tasks  -->
             <!-- --------------------------------------------------------------------------- -->
+            <label for="blockedBy" class="accesskey-first">Before this</label>
             <Dependency
                 type="blockedBy"
-                labelText="Before this"
                 {task}
                 {editableTask}
                 {allTasks}
                 {_onDescriptionKeyDown}
-                accesskey={accesskey('b')}
+                {accesskey}
+                accesskeyLetter="b"
                 placeholder="Search for tasks that the task being edited depends on..."
             />
 
             <!-- --------------------------------------------------------------------------- -->
             <!--  Blocking Tasks  -->
             <!-- --------------------------------------------------------------------------- -->
+            <label for="blocking">Aft<span class="accesskey">e</span>r this</label>
             <Dependency
                 type="blocking"
-                labelText="After this"
                 {task}
                 {editableTask}
                 {allTasks}
                 {_onDescriptionKeyDown}
-                accesskey={accesskey('e')}
+                {accesskey}
+                accesskeyLetter="e"
                 placeholder="Search for tasks that depend on this task being done..."
             />
         {:else}
@@ -592,7 +592,7 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Status  -->
         <!-- --------------------------------------------------------------------------- -->
-        <label for="status">{@html labelContentWithAccessKey('Status', accesskey('u'))}</label>
+        <label for="status">Stat<span class="accesskey">u</span>s</label>
         <!-- svelte-ignore a11y-accesskey -->
         <select
             bind:value={statusSymbol}
@@ -609,6 +609,7 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Created Date  -->
         <!-- --------------------------------------------------------------------------- -->
+        <label for="created" class="accesskey-first">Created</label>
         <DateEditor
             id="created"
             dateSymbol={createdDateSymbol}
@@ -621,6 +622,11 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Done Date  -->
         <!-- --------------------------------------------------------------------------- -->
+        {#if withAccessKeys}
+            <label for="done">Done (<span class="accesskey">x</span>)</label>
+        {:else}
+            <label for="done">Done</label>
+        {/if}
         <DateEditor
             id="done"
             dateSymbol={doneDateSymbol}
@@ -633,6 +639,11 @@ Availability of access keys:
         <!-- --------------------------------------------------------------------------- -->
         <!--  Cancelled Date  -->
         <!-- --------------------------------------------------------------------------- -->
+        {#if withAccessKeys}
+            <label for="cancelled">Cancelled (<span class="accesskey">-</span>)</label>
+        {:else}
+            <label for="cancelled">Cancelled</label>
+        {/if}
         <DateEditor
             id="cancelled"
             dateSymbol={cancelledDateSymbol}
diff --git a/src/ui/EditTaskHelpers.ts b/src/ui/EditTaskHelpers.ts
deleted file mode 100644
index ffdd77fe..00000000
--- a/src/ui/EditTaskHelpers.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { capitalizeFirstLetter } from '../lib/StringHelpers';
-
-/**
- * Returns contents for a `<label>` HTML element.
- *
- * If the {@link accessKey} is found in the label text (case-insensitive), it will be given `accesskey` class
- * within a `<span>` HTML element. Else the access key will be added at the end of the label text,
- * surrounded with round brackets.
- *
- * The first letter of the label will be capitalised.
- *
- * @param labelText to be displayed in the <label> HTML element.
- * @param accessKey optional access key. Set to null if not needed.
- */
-export function labelContentWithAccessKey(labelText: string, accessKey: string | null) {
-    if (accessKey === null) {
-        return capitalizeFirstLetter(labelText);
-    }
-
-    const accessKeyIndex = labelText.toLowerCase().indexOf(accessKey.toLowerCase());
-    if (accessKeyIndex === -1) {
-        return `${capitalizeFirstLetter(labelText)} (<span class="accesskey">${accessKey.toLowerCase()}</span>)`;
-    }
-
-    let labelContent = labelText.substring(0, accessKeyIndex);
-    labelContent += '<span class="accesskey">';
-
-    if (accessKeyIndex === 0) {
-        labelContent += labelText.substring(accessKeyIndex, accessKeyIndex + 1).toUpperCase();
-    } else {
-        labelContent += labelText.substring(accessKeyIndex, accessKeyIndex + 1);
-    }
-
-    labelContent += '</span>';
-    labelContent += labelText.substring(accessKeyIndex + 1);
-    labelContent = capitalizeFirstLetter(labelContent);
-    return labelContent;
-}


Test patch:
diff --git a/tests/Suggestor/Suggestor.test.ts b/tests/Suggestor/Suggestor.test.ts
index 65f87048..588e9320 100644
--- a/tests/Suggestor/Suggestor.test.ts
+++ b/tests/Suggestor/Suggestor.test.ts
@@ -171,6 +171,11 @@ describe.each([
         }
     }
 
+    function shouldOnlyOfferDefaultSuggestionsForEndOfLine(line: string, allTasks: Task[] = []) {
+        const suggestions = buildSuggestionsForEndOfLine(line, allTasks);
+        shouldOnlyOfferDefaultSuggestions(suggestions);
+    }
+
     const {
         dueDateSymbol,
         scheduledDateSymbol,
@@ -248,7 +253,7 @@ describe.each([
         expect(suggestions[0].displayText).not.toContain('created today');
     });
 
-    describe('suggestions for dependency fields', () => {
+    describe('suggestions for dependency field ID', () => {
         it('should offer "id" then "depends on" if user typed "id"', () => {
             const line = '- [ ] some task id';
             shouldStartWithSuggestionsEqualling(line, [`${idSymbol} Task ID`, `${dependsOnSymbol} Task depends on ID`]);
@@ -259,10 +264,30 @@ describe.each([
             shouldStartWithSuggestionsEqualling(line, ['Auto Generate Unique ID']);
         });
 
+        it('should offer to generate unique id if the id symbol is already present', () => {
+            const line = `- [ ] some task ${idSymbol} 1234`;
+            shouldOnlyOfferDefaultSuggestionsForEndOfLine(line);
+        });
+    });
+
+    describe('suggestions for dependency field ', () => {
+        // Make tests more thorough by allowing tests to ensure that there are no more dependency suggestions
+        // after the ones we supply to shouldStartWithSuggestionsEqualling():
+        const defaultSuggestion = `${dueDateSymbol} due date`;
+
+        function shouldStartWithSuggestedTasks(line: string, selectedTasks: Task[], allTasks: Task[]) {
+            const selectedTaskLabels = selectedTasks.map((task) => suggestionLabel(task));
+            shouldStartWithSuggestionsEqualling(line, [...selectedTaskLabels, defaultSuggestion], allTasks);
+        }
+
+        function suggestionLabel(taskxy: Task) {
+            return `${taskxy.descriptionWithoutTags} - From: ${taskxy.file.filename}`;
+        }
+
         it('should offer to depend on only task in vault, and include its filename in suggestion if user typed "id"', () => {
             const line = `- [ ] some task ${dependsOnSymbol} `;
             const taskToDependOn = TaskBuilder.createFullyPopulatedTask();
-            shouldStartWithSuggestionsEqualling(line, ['Do exercises - From: fileName.md'], [taskToDependOn]);
+            shouldStartWithSuggestionsEqualling(line, [suggestionLabel(taskToDependOn)], [taskToDependOn]);
         });
 
         // TODO should not offer to depend on self
@@ -273,52 +298,106 @@ describe.each([
 
         describe('suggesting additional dependencies', () => {
             const taskBuilder = new TaskBuilder().path('root/dir 1/dir 2/file-name.md');
-            const allTasks = [
-                // force line break
-                taskBuilder.description('1').id('1234').build(),
-                taskBuilder.description('2').id('5678').build(),
-            ];
+            const allTasks: Task[] = [];
+
+            // Function to create a task and append it to the allTasks array
+            function createAndAddTask(description: string, id: string): Task {
+                const task = taskBuilder.description(description).id(id).build();
+                allTasks.push(task);
+                return task;
+            }
 
-            const suggestTask1 = '1 - From: file-name.md';
-            const suggestTask2 = '2 - From: file-name.md';
+            // Create tasks and add them to allTasks
+            // Variable names based on ID, not description:
+            const taskxy = createAndAddTask('x_y', 'xy');
+            const task1234 = createAndAddTask('1', '1234');
+            const task5678 = createAndAddTask('2', '5678');
 
             it('should suggest all tasks when there is no existing ID after dependsOn', () => {
                 const line = `- [ ] some task ${dependsOnSymbol} `;
-                shouldStartWithSuggestionsEqualling(line, [suggestTask1, suggestTask2], allTasks);
+                shouldStartWithSuggestedTasks(line, [taskxy, task1234, task5678], allTasks);
             });
 
             it('should offer tasks containing the search string, if given a partial ID', () => {
                 // 1 does not match any of the existing IDs, so is presumed to be a substring to search for.
                 const line = `- [ ] some task ${dependsOnSymbol} 1`;
-                shouldStartWithSuggestionsEqualling(line, [suggestTask1], allTasks);
+                shouldStartWithSuggestedTasks(line, [task1234], allTasks);
+            });
+
+            it('should offer tasks containing the search string, if given a partial ID even when no space between symbol and ID', () => {
+                // 1 does not match any of the existing IDs, so is presumed to be a substring to search for.
+                const line = `- [ ] some task ${dependsOnSymbol}1`;
+                shouldStartWithSuggestedTasks(line, [task1234], allTasks);
+            });
+
+            it('should offer tasks containing underscore in description, if given as partial ID', () => {
+                // x_ does not match any of the existing IDs, so is presumed to be a substring to search for.
+                const line = `- [ ] some task ${dependsOnSymbol} x_`;
+                shouldStartWithSuggestedTasks(line, [taskxy], allTasks);
+            });
+
+            it('should offer tasks containing underscore in description, if given as second partial ID', () => {
+                // x_ does not match any of the existing IDs, so is presumed to be a substring to search for.
+                const line = `- [ ] some task ${dependsOnSymbol} 1234,x_`;
+                shouldStartWithSuggestedTasks(line, [taskxy], allTasks);
             });
 
             it('should only offer tasks not already depended upon - with 1 existing dependency', () => {
                 const line = `- [ ] some task ${dependsOnSymbol} 1234,`;
-                shouldStartWithSuggestionsEqualling(line, [suggestTask2], allTasks);
+                shouldStartWithSuggestedTasks(line, [taskxy, task5678], allTasks);
             });
 
             it('should offer tasks when first existing dependency id has hyphen and underscore', () => {
                 const line = `- [ ] some task ${dependsOnSymbol} 1_2-3,`;
-                shouldStartWithSuggestionsEqualling(line, [suggestTask1, suggestTask2], allTasks);
+                shouldStartWithSuggestedTasks(line, [taskxy, task1234, task5678], allTasks);
+            });
+
+            it('should only offer tasks not already depended upon - and allows spaces around commas', () => {
+                const line = `- [ ] some task ${dependsOnSymbol} xy , 5678 , `;
+                shouldStartWithSuggestedTasks(line, [task1234], allTasks);
             });
 
             it('should only offer tasks not already depended upon - with all tasks already depended on', () => {
-                const line = `- [ ] some task ${dependsOnSymbol} 1234,5678,`;
-                const suggestions = buildSuggestionsForEndOfLine(line, allTasks);
-                shouldOnlyOfferDefaultSuggestions(suggestions);
+                const line = `- [ ] some task ${dependsOnSymbol} xy,1234,5678,`;
+                shouldOnlyOfferDefaultSuggestionsForEndOfLine(line, allTasks);
+            });
+
+            it('should use equality to check for existing IDs, not containment', () => {
+                const taskWithId123 = taskBuilder.description('3').id('123').build();
+                const line = `- [ ] some task ${dependsOnSymbol} xy,1234,5678,`;
+
+                // 123 is only a substring of one of the ID 1234 which is already depended on, not an exat match.
+                // So it should not count as already matched.
+                shouldStartWithSuggestedTasks(line, [taskWithId123], [taskWithId123, ...allTasks]);
+            });
+
+            it('should find non-exact matches for multi-word search strings', () => {
+                const feedBaby = taskBuilder.description('Feed the baby').id('bby').build();
+                const feedCat = taskBuilder.description('Feed the cat').id('ct').build();
+
+                const line = `- [ ] some task ${dependsOnSymbol} xy,1234,5678,feed baby`;
+                // Search string 'feed baby' should match only 'Feed the baby':
+                shouldStartWithSuggestedTasks(line, [feedBaby], [feedBaby, feedCat, ...allTasks]);
+            });
+
+            it.failing('should allow punctuation in search strings', () => {
+                const peace1 = taskBuilder.description('World peace!').id('peace1').build();
+
+                // Don't match a task that has peace present, but without the exclamation mark
+                const peace2 = taskBuilder.description('Peacefulness').id('peace2').build();
+
+                const line = `- [ ] some task ${dependsOnSymbol} xy,1234,5678,peace!`;
+                shouldStartWithSuggestedTasks(line, [peace1], [peace1, peace2, ...allTasks]);
             });
 
             it('should not offer any tasks if there is not a comma after existing depends IDs', () => {
                 const line = `- [ ] some task ${dependsOnSymbol} 1234,5678`;
-                const suggestions = buildSuggestionsForEndOfLine(line, allTasks);
-                shouldOnlyOfferDefaultSuggestions(suggestions);
+                shouldOnlyOfferDefaultSuggestionsForEndOfLine(line, allTasks);
             });
 
             it('should not offer tasks if "IDs" are separated by spaces', () => {
                 const line = `- [ ] some task ${dependsOnSymbol} 1234 5678`;
-                const suggestions = buildSuggestionsForEndOfLine(line, allTasks);
-                shouldOnlyOfferDefaultSuggestions(suggestions);
+                shouldOnlyOfferDefaultSuggestionsForEndOfLine(line, allTasks);
             });
         });
     });
diff --git a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html
index 0f4ae338..a564fb67 100644
--- a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html
+++ b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot.approved.html
@@ -1,5 +1,5 @@
 <div>
-  <form class="tasks-modal">
+  <form class="tasks-modal with-accesskeys">
     <section class="tasks-modal-description-section">
       <label for="description">
         Descrip
@@ -70,10 +70,7 @@
     </section>
     <hr />
     <section class="tasks-modal-dates-section">
-      <label for="recurrence">
-        <span class="accesskey">R</span>
-        ecurs
-      </label>
+      <label for="recurrence" class="accesskey-first">Recurs</label>
       <input
         id="recurrence"
         type="text"
@@ -84,10 +81,7 @@
         🔁
         <i>not recurring</i>
       </code>
-      <label for="due">
-        <span class="accesskey">D</span>
-        ue
-      </label>
+      <label for="due" class="accesskey-first">Due</label>
       <input
         id="due"
         type="text"
@@ -98,10 +92,7 @@
         📅
         <i>no due date</i>
       </code>
-      <label for="scheduled">
-        <span class="accesskey">S</span>
-        cheduled
-      </label>
+      <label for="scheduled" class="accesskey-first">Scheduled</label>
       <input
         id="scheduled"
         type="text"
@@ -130,18 +121,15 @@
       <div class="future-dates-only">
         <label for="forwardOnly">
           Only
-          <span class="accesskey">f</span>
-          uture dates:
+          <span class="accesskey-first">future</span>
+          dates:
         </label>
         <input id="forwardOnly" type="checkbox" class="task-list-item-checkbox tasks-modal-checkbox" accesskey="f" />
       </div>
     </section>
     <hr />
     <section class="tasks-modal-dependencies-section">
-      <label for="blockedBy">
-        <span class="accesskey">B</span>
-        efore this
-      </label>
+      <label for="blockedBy" class="accesskey-first">Before this</label>
       <span>
         <input
           accesskey="b"
@@ -211,10 +199,7 @@
         <option value="x">Done [x]</option>
         <option value="-">Cancelled [-]</option>
       </select>
-      <label for="created">
-        <span class="accesskey">C</span>
-        reated
-      </label>
+      <label for="created" class="accesskey-first">Created</label>
       <input
         id="created"
         type="text"
diff --git a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html
index 26a0de65..7fdc8150 100644
--- a/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html
+++ b/tests/ui/EditTask.test.Edit_Modal_HTML_snapshot_tests_should_match_snapshot_-_without_access_keys.approved.html
@@ -1,7 +1,11 @@
 <div>
   <form class="tasks-modal">
     <section class="tasks-modal-description-section">
-      <label for="description">Description</label>
+      <label for="description">
+        Descrip
+        <span class="accesskey">t</span>
+        ion
+      </label>
       <textarea id="description" class="tasks-modal-description" placeholder="Take out the trash"></textarea>
     </section>
     <section class="tasks-modal-priority-section">
@@ -9,77 +13,99 @@
       <div class="task-modal-priority-option-container">
         <input type="radio" id="priority-lowest" value="lowest" />
         <label for="priority-lowest">
-          <span>Lowest</span>
+          <span>L</span>
+          <span class="accesskey">o</span>
+          <span>west</span>
           <span>⏬</span>
         </label>
       </div>
       <div class="task-modal-priority-option-container">
         <input type="radio" id="priority-low" value="low" />
         <label for="priority-low">
-          <span>Low</span>
+          <span></span>
+          <span class="accesskey">L</span>
+          <span>ow</span>
           <span>🔽</span>
         </label>
       </div>
       <div class="task-modal-priority-option-container">
         <input type="radio" id="priority-none" value="none" />
-        <label for="priority-none"><span>Normal</span></label>
+        <label for="priority-none">
+          <span></span>
+          <span class="accesskey">N</span>
+          <span>ormal</span>
+        </label>
       </div>
       <div class="task-modal-priority-option-container">
         <input type="radio" id="priority-medium" value="medium" />
         <label for="priority-medium">
-          <span>Medium</span>
+          <span></span>
+          <span class="accesskey">M</span>
+          <span>edium</span>
           <span>🔼</span>
         </label>
       </div>
       <div class="task-modal-priority-option-container">
         <input type="radio" id="priority-high" value="high" />
         <label for="priority-high">
-          <span>High</span>
+          <span></span>
+          <span class="accesskey">H</span>
+          <span>igh</span>
           <span>⏫</span>
         </label>
       </div>
       <div class="task-modal-priority-option-container">
         <input type="radio" id="priority-highest" value="highest" />
         <label for="priority-highest">
-          <span>Highest</span>
+          <span>H</span>
+          <span class="accesskey">i</span>
+          <span>ghest</span>
           <span>🔺</span>
         </label>
       </div>
     </section>
     <hr />
     <section class="tasks-modal-dates-section">
-      <label for="recurrence">Recurs</label>
+      <label for="recurrence" class="accesskey-first">Recurs</label>
       <input id="recurrence" type="text" class="tasks-modal-date-input" placeholder="Try 'every day when done'" />
       <code class="tasks-modal-parsed-date">
         🔁
         <i>not recurring</i>
       </code>
-      <label for="due">Due</label>
+      <label for="due" class="accesskey-first">Due</label>
       <input id="due" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
       <code class="tasks-modal-parsed-date">
         📅
         <i>no due date</i>
       </code>
-      <label for="scheduled">Scheduled</label>
+      <label for="scheduled" class="accesskey-first">Scheduled</label>
       <input id="scheduled" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
       <code class="tasks-modal-parsed-date">
         ⏳
         <i>no scheduled date</i>
       </code>
-      <label for="start">Start</label>
+      <label for="start">
+        St
+        <span class="accesskey">a</span>
+        rt
+      </label>
       <input id="start" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
       <code class="tasks-modal-parsed-date">
         🛫
         <i>no start date</i>
       </code>
       <div class="future-dates-only">
-        <label for="forwardOnly">Only future dates:</label>
+        <label for="forwardOnly">
+          Only
+          <span class="accesskey-first">future</span>
+          dates:
+        </label>
         <input id="forwardOnly" type="checkbox" class="task-list-item-checkbox tasks-modal-checkbox" />
       </div>
     </section>
     <hr />
     <section class="tasks-modal-dependencies-section">
-      <label for="blockedBy">Before this</label>
+      <label for="blockedBy" class="accesskey-first">Before this</label>
       <span>
         <input
           id="blockedBy"
@@ -104,7 +130,11 @@
           tabindex="-1"
           src="about:blank"></iframe>
       </span>
-      <label for="blocking">After this</label>
+      <label for="blocking">
+        Aft
+        <span class="accesskey">e</span>
+        r this
+      </label>
       <span>
         <input
           id="blocking"
@@ -132,14 +162,18 @@
     </section>
     <hr />
     <section class="tasks-modal-dates-section">
-      <label for="status">Status</label>
+      <label for="status">
+        Stat
+        <span class="accesskey">u</span>
+        s
+      </label>
       <select id="status-type" class="tasks-modal-status-selector">
         <option value=" ">Todo [ ]</option>
         <option value="/">In Progress [/]</option>
         <option value="x">Done [x]</option>
         <option value="-">Cancelled [-]</option>
       </select>
-      <label for="created">Created</label>
+      <label for="created" class="accesskey-first">Created</label>
       <input id="created" type="text" class="tasks-modal-date-input" placeholder="Try 'Mon' or 'tm' then space" />
       <code class="tasks-modal-parsed-date">
         ➕
diff --git a/tests/ui/EditTaskHelpers.test.ts b/tests/ui/EditTaskHelpers.test.ts
deleted file mode 100644
index 51a714be..00000000
--- a/tests/ui/EditTaskHelpers.test.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { labelContentWithAccessKey } from '../../src/ui/EditTaskHelpers';
-
-describe('labelContentWithAccessKey() tests', () => {
-    it.each([
-        [
-            //should make...
-            'first letter is the access key f, not the second',
-            'f',
-            '<span class="accesskey">F</span>irst letter is the access key f, not the second',
-        ],
-        [
-            'should make this x the access key even if it is in the middle',
-            'x',
-            'Should make this <span class="accesskey">x</span> the access key even if it is in the middle',
-        ],
-        [
-            'should keep the Capitalised letter as the access key',
-            'C',
-            'Should keep the <span class="accesskey">C</span>apitalised letter as the access key',
-        ],
-        [
-            'should make this y the access key too even if the parameter is a capital Y',
-            'Y',
-            'Should make this <span class="accesskey">y</span> the access key too even if the parameter is a capital Y',
-        ],
-        [
-            'should add the access key at the end of this text',
-            'z',
-            'Should add the access key at the end of this text (<span class="accesskey">z</span>)',
-        ],
-        [
-            'should add the access key at the end of this text even if the parameter is capital',
-            'Z',
-            'Should add the access key at the end of this text even if the parameter is capital (<span class="accesskey">z</span>)',
-        ],
-        ['should not add an access key span here', null, 'Should not add an access key span here'],
-    ])("label text '%s' with access key '%s' should have label content '%s'", (labelText, accessKey, labelContent) => {
-        expect(labelContentWithAccessKey(labelText, accessKey)).toEqual(labelContent);
-    });
-});