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

Issue:
Auto-suggest pops "created today" far too often
### Please check that this issue hasn't been reported before.

- [X] I searched previous [Bug Reports](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22+sort%3Acomments-desc) didn't find any similar reports.

### Expected Behavior

1. Scenario 1: when typing a task with Auto Suggest turned on, typing "due" should enter the Due Date emoji, then continuing to type "today" should offer to complete today's date.
2. Scenario 2: when typing a task that includes the word "today", no suggestion should pop.

### Current behaviour

After the merge of #1872, the auto-suggest menu behaves inconsistently to before.

1. In Scenario 1 above, typing "today" after inserting the Due Date emoji suggests today's date (correctly), but also suggests to add "created today". Pressing Enter on "created today" leaves an invalid task syntax like '📅 ➕ 2023-04-18'.
2. In Scenario 2, starting to type "today" in a task line pops up the suggestion "created today" in a way that is inconsistent with other suggestions (there are suggestions for "due today", "started today" etc).

### Steps to reproduce

As described above. For example to recreate scenario 2, with a global filter `#t`, typing `- [ ] #t tod` pops up an unwanted suggestion.

### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [X] Linux
- [ ] macOS
- [ ] Windows

### Obsidian Version

1.2.3

### Tasks Plugin Version

Commit dcd1

### Checks

- [X] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

To make "created date" completely consistent with other date fields, it needs to be suggested in `Suggestor.getPossibleComponentSuggestions` in the same format as other fields: "➕ created", adding just the ➕ sign, and at the next step a date would be suggested.
However, I assume it was not done this way because "created today" is by far the most common use case, so instead of suggesting "➕ created", the suggestion is for "➕ created today (<date>)". However, this matches far more strings than the original result of this method was originally intended to.
If it's desirable to keep a single "created today" suggestion and not behave similarly to other fields, my proposed solution is to change the `SuggestInfo` type to also include an optional `textToMatch` field. If this field is present, the code that does the matching (`filteredSuggestions` inside `makeDefaultSuggestionBuilder`) would use `suggestInfo.textToMatch` rather than `suggestInfo.displayText`.
This way, in `getPossibleComponentSuggestions` where the "created" field is added, we can change that suggestion to:
```
{
            textToMatch: 'created',
            displayText: `${symbols.createdDateSymbol} created today (${formattedDate})`,
            appendText: `${symbols.createdDateSymbol} ${formattedDate} `,
}
```

Pull request description:
fix: created date suggestion appearing too often
<!--- Provide a general summary of your changes in the Title above -->

# Description

Prevents the "Created today" suggestions to appear when the user types "today", which was causing it to match too often.

## Motivation and Context

Fixes #1893  

## How has this been tested?

Tested in the sample vault, as shown in the screen cap. Tested with the new Dataview format as well. I don't believe the changes affect any other area of the code.

## Screenshots (if appropriate)

![task-suggestion-today](https://user-images.githubusercontent.com/6631995/232960837-099ccd67-6a60-4f45-9d05-cf4a200a1e4c.gif)


## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [ ] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))
- [ ] **Contributing Guidelines** (prefix: `contrib` - any improvements to documentation content **for contributors** - see [Contributing to Tasks](https://publish.obsidian.md/tasks-contributing/))

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [x] 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/docs/Getting Started/Auto-Suggest.md b/docs/Getting Started/Auto-Suggest.md
index 2aa489c5..079e39e4 100644
--- a/docs/Getting Started/Auto-Suggest.md
+++ b/docs/Getting Started/Auto-Suggest.md
@@ -194,7 +194,7 @@ Similarly, you can type some fraction of the word `start` (of whatever length is
 | 📅 due date                   | 📅                         |
 | ⏳ scheduled date             | ⏳                         |
 | 🛫 start date                 | 🛫                         |
-| ➕ created today (2022-07-11) | ➕ 2022-07-11              |
+| ➕ created                    | ➕ 2022-07-11              |
 | today (2022-07-11)            | 2022-07-11                 |
 | tomorrow (2022-07-12)         | 2022-07-12                 |
 | Sunday (2022-07-17)           | 2022-07-17                 |
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index 8c91d70b..0d1139d4 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -20,36 +20,8 @@ export function makeDefaultSuggestionBuilder(symbols: DefaultTaskSerializerSymbo
         // 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, 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);
+        // Step 3: add task property suggestions ('due', 'recurrence' etc)
+        suggestions = suggestions.concat(addTaskPropertySuggestions(line, cursorPos, settings, symbols));
 
         // 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
@@ -73,60 +45,91 @@ export function makeDefaultSuggestionBuilder(symbols: DefaultTaskSerializerSymbo
 /*
  * Get suggestions for generic task components, e.g. a priority or a 'due' symbol
  */
-function getPossibleComponentSuggestions(
+function addTaskPropertySuggestions(
     line: string,
+    cursorPos: number,
     _settings: Settings,
     symbols: DefaultTaskSerializerSymbols,
 ): SuggestInfo[] {
     const hasPriority = (line: string) =>
         Object.values(symbols.prioritySymbols).some((value) => value.length > 0 && line.includes(value));
 
-    const suggestions: SuggestInfo[] = [];
+    const genericSuggestions: SuggestInfo[] = [];
 
     if (!line.includes(symbols.dueDateSymbol))
-        suggestions.push({
+        genericSuggestions.push({
             displayText: `${symbols.dueDateSymbol} due date`,
             appendText: `${symbols.dueDateSymbol} `,
         });
     if (!line.includes(symbols.startDateSymbol))
-        suggestions.push({
+        genericSuggestions.push({
             displayText: `${symbols.startDateSymbol} start date`,
             appendText: `${symbols.startDateSymbol} `,
         });
     if (!line.includes(symbols.scheduledDateSymbol))
-        suggestions.push({
+        genericSuggestions.push({
             displayText: `${symbols.scheduledDateSymbol} scheduled date`,
             appendText: `${symbols.scheduledDateSymbol} `,
         });
     if (!hasPriority(line)) {
-        suggestions.push({
+        genericSuggestions.push({
             displayText: `${symbols.prioritySymbols.High} high priority`,
             appendText: `${symbols.prioritySymbols.High} `,
         });
-        suggestions.push({
+        genericSuggestions.push({
             displayText: `${symbols.prioritySymbols.Medium} medium priority`,
             appendText: `${symbols.prioritySymbols.Medium} `,
         });
-        suggestions.push({
+        genericSuggestions.push({
             displayText: `${symbols.prioritySymbols.Low} low priority`,
             appendText: `${symbols.prioritySymbols.Low} `,
         });
     }
     if (!line.includes(symbols.recurrenceSymbol))
-        suggestions.push({
+        genericSuggestions.push({
             displayText: `${symbols.recurrenceSymbol} recurring (repeat)`,
             appendText: `${symbols.recurrenceSymbol} `,
         });
     if (!line.includes(symbols.createdDateSymbol)) {
         const parsedDate = DateParser.parseDate('today', true);
         const formattedDate = parsedDate.format(TaskRegularExpressions.dateFormat);
-        suggestions.push({
+        genericSuggestions.push({
+            // We don't want this to match when the user types "today"
+            textToMatch: `${symbols.createdDateSymbol} created`,
             displayText: `${symbols.createdDateSymbol} created today (${formattedDate})`,
             appendText: `${symbols.createdDateSymbol} ${formattedDate} `,
         });
     }
 
-    return suggestions;
+    // 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);
+    const matchingSuggestions: SuggestInfo[] = [];
+    if (wordMatch && wordMatch.length > 0) {
+        const wordUnderCursor = wordMatch[0];
+        if (wordUnderCursor.length >= Math.max(1, _settings.autoSuggestMinMatch)) {
+            const filteredSuggestions = genericSuggestions.filter((suggestInfo) => {
+                const textToMatch = suggestInfo.textToMatch || suggestInfo.displayText;
+                return textToMatch.toLowerCase().includes(wordUnderCursor.toLowerCase());
+            });
+            for (const filtered of filteredSuggestions) {
+                matchingSuggestions.push({
+                    suggestionType: 'match',
+                    displayText: filtered.displayText,
+                    appendText: filtered.appendText,
+                    insertAt: wordMatch.index,
+                    insertSkip: wordUnderCursor.length,
+                });
+            }
+        }
+    }
+    // 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 (matchingSuggestions.length === 0 && _settings.autoSuggestMinMatch === 0) return genericSuggestions;
+
+    return matchingSuggestions;
 }
 
 /*
@@ -317,10 +320,9 @@ function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Set
  * Matches a string with a regex according to a position (typically of a cursor).
  * Will return a result only if a match exists and the given position is part of it.
  */
-export function matchByPosition(s: string, r: RegExp, position: number): RegExpMatchArray {
+export function matchByPosition(s: string, r: RegExp, position: number): RegExpMatchArray | void {
     const matches = s.matchAll(r);
     for (const match of matches) {
         if (match?.index && match.index <= position && position <= match.index + match[0].length) return match;
     }
-    return [];
 }
diff --git a/src/Suggestor/index.ts b/src/Suggestor/index.ts
index 1c135a1a..b2f198aa 100644
--- a/src/Suggestor/index.ts
+++ b/src/Suggestor/index.ts
@@ -5,14 +5,16 @@ import type { Settings } from '../Config/Settings';
  */
 export type SuggestInfo = {
     suggestionType?: 'match' | 'default' | 'empty';
-    // What to display to the user
+    /** What to display to the user  */
     displayText: string;
-    // What to append to the note
+    /** 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)
+    /** 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)
+    /** How many characters to skip from the original line (e.g. if replacing existing text) */
     insertSkip?: number;
+    /** Text to match with the user input if matching against the display text is not desirable */
+    textToMatch?: string;
 };
 
 /*


Test patch:
diff --git a/tests/Suggestor/Suggestor.test.ts b/tests/Suggestor/Suggestor.test.ts
index 436f1aa5..da21dd3b 100644
--- a/tests/Suggestor/Suggestor.test.ts
+++ b/tests/Suggestor/Suggestor.test.ts
@@ -14,9 +14,13 @@ window.moment = moment;
 // Set predictable date for all tests in this file
 const mockDate = new Date(moment('2022-07-11 15:00').valueOf());
 
-jest.spyOn(chrono, 'parseDate').mockImplementation(
-    (text, _, options) => chrono.en.casual.parseDate(text, mockDate, options)!,
-);
+const chronoSpy = jest
+    .spyOn(chrono, 'parseDate')
+    .mockImplementation((text, _, options) => chrono.en.casual.parseDate(text, mockDate, options)!);
+
+afterAll(() => {
+    chronoSpy.mockRestore();
+});
 
 describe.each([
     { name: 'emoji', symbols: DEFAULT_SYMBOLS },
@@ -98,6 +102,19 @@ describe.each([
         expect(suggestions[1].displayText).toEqual('every day');
     });
 
+    it('matches created property suggestion when user types "created" but not "today"', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        let line = '- [ ] some task cr';
+        let suggestions: SuggestInfo[] = buildSuggestions(line, 18, originalSettings);
+        expect(suggestions[0].displayText).toEqual(`${createdDateSymbol} created today (2022-07-11)`);
+
+        line = '- [ ] some task tod';
+        suggestions = buildSuggestions(line, 19, originalSettings);
+        expect(suggestions[0].suggestionType).toEqual('empty');
+        expect(suggestions[0].displayText).not.toContain('created today');
+    });
+
     // Until the maximum date suggestion number is no longer hardcoded,
     // it's not possible to output all dates. Skipping for now.
     it.skip('show all suggested text', () => {