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

Issue:
Tasks should retain knowledge of whether the task line starts with hyphen or asterisk
### 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

When I have a list of tasks starting with an asterisk/star, and Tasks modifies them in any way, I expect that the asterisk should remain as the starting character on the line.



### Current behaviour

When I have a list of tasks starting with an asterisk/star, and Tasks modifies them in any way, it converts the asterisk to a hyphen.

If the task is one of several, this totally messes up the display:

Starting point:

```
1. [ ] #task stuff 1
1. [ ] #task Stuff 2
1. [ ] #task Stuff 3
```

Starting point in Reading view:

![image](https://user-images.githubusercontent.com/4840096/205488241-406ab810-f618-48a2-aab2-56373d7201d4.png)


After completing the middle task with Tasks:

```
1. [ ] #task stuff 1
- [x] #task Stuff 2 ✅ 2022-12-04
1. [ ] #task Stuff 3
```

![image](https://user-images.githubusercontent.com/4840096/205488256-dc247c87-df72-4eff-b9a1-21ff289d5c80.png)






### Steps to reproduce

1. Open Obsidian in a vault with Tasks enabled
2. Create any old note
3. Paste the following Markdown in:

```
## List 1

- [ ] #task stuff 1 a
- [ ] #task Stuff 2 a

## List 2

* [ ] #task stuff 1 b
* [ ] #task Stuff 2 b
```

4. Switch to Reading view
5. Complete all the tasks

#### Actual Result:

```
## List 1

- [x] #task stuff 1 a ✅ 2022-12-04
- [x] #task Stuff 2 a ✅ 2022-12-04

## List 2

- [x] #task stuff 1 b ✅ 2022-12-04
- [x] #task Stuff 2 b ✅ 2022-12-04
```

Expected result:

```
## List 1

- [x] #task stuff 1 a ✅ 2022-12-04
- [x] #task Stuff 2 a ✅ 2022-12-04

## List 2

* [x] #task stuff 1 b ✅ 2022-12-04
* [x] #task Stuff 2 b ✅ 2022-12-04
```


### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

1.0.3

### Tasks Plugin Version

1.19.0

### Checks

- [ ] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

I'm unsure of the best implementation, but here are some thoughts.

Where Tasks retains the whitespace indentation for a Task, it should also retain the prefix character.

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/42783a468c6e0034efeb020384e410405a9a6ee2/src/Task.ts#L128

This new information should be in a different variable from `Task.indentation` to avoid breaking features like `exclude sub-items`.


Pull request:
fix: Editing & toggling task starting * or 1. reverted them to -
<!--- Provide a general summary of your changes in the Title above -->

# Description

When parsing task lines, the code now retains the list marker, which will be one of:

- a hyphen: `-`
- an asterisk: `*`
- a number with a full-stop: `5.`, for example

Changes in behaviour:

- This fixes all the example problems in #1371
    - toggling tasks on lines beginning `*` retains the `*`
    - toggling tasks on lines beginning `1.` retains the `1.`
- Editing tasks with any of the above list markers retains the marker, instead of converting it to `-`

## Motivation and Context

Fixes #1371 - Tasks should retain knowledge of whether the task line starts with hyphen or asterisk.

And also improves the behaviour of #1363 - Add support for numbered checkboxes.

## How has this been tested?

- Extensive test added, both here and in previous PRs.
- Manually test in Obsidian, including testing the samples in #1371.

## 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] **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`.
- [x] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)


Patch:
diff --git a/src/Commands/CreateOrEditTaskParser.ts b/src/Commands/CreateOrEditTaskParser.ts
index 94ca66ff..77c73c3e 100644
--- a/src/Commands/CreateOrEditTaskParser.ts
+++ b/src/Commands/CreateOrEditTaskParser.ts
@@ -61,10 +61,10 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
     }
 
     const indentation: string = nonTaskMatch[1];
-    const listMarker = '-';
-    const statusString: string = nonTaskMatch[3] ?? ' ';
+    const listMarker = nonTaskMatch[2] ?? '-';
+    const statusString: string = nonTaskMatch[4] ?? ' ';
     const status = statusString === ' ' ? Status.TODO : Status.DONE;
-    let description: string = nonTaskMatch[4];
+    let description: string = nonTaskMatch[5];
 
     const blockLinkMatch = line.match(TaskRegularExpressions.blockLinkRegex);
     const blockLink = blockLinkMatch !== null ? blockLinkMatch[0] : '';
diff --git a/src/Commands/ToggleDone.ts b/src/Commands/ToggleDone.ts
index 67487882..2e2a360a 100644
--- a/src/Commands/ToggleDone.ts
+++ b/src/Commands/ToggleDone.ts
@@ -70,14 +70,15 @@ export const toggleLine = (line: string, path: string) => {
         // 1. a regular checklist item
         // 2. a list item
         // 3. a simple text line
+        // 4. a standard task, but which does not contain the global filter, to be toggled, but no done date added.
 
         // The task regex will match checklist items.
         const regexMatch = line.match(TaskRegularExpressions.taskRegex);
         if (regexMatch !== null) {
             // Toggle the status of the checklist item.
-            const statusString = regexMatch[2].toLowerCase(); // Note for future: I do not think this toLowerCase is necessary and there is an issue about how it breaks some theme or snippet.
+            const statusString = regexMatch[3].toLowerCase(); // Note for future: I do not think this toLowerCase is necessary and there is an issue about how it breaks some theme or snippet.
             const newStatusString = statusString === ' ' ? 'x' : ' ';
-            toggledLine = line.replace(TaskRegularExpressions.taskRegex, `$1- [${newStatusString}] $3`);
+            toggledLine = line.replace(TaskRegularExpressions.taskRegex, `$1- [${newStatusString}] $4`);
         } else if (TaskRegularExpressions.listItemRegex.test(line)) {
             // Convert the list item to a checklist item.
             toggledLine = line.replace(TaskRegularExpressions.listItemRegex, '$1$2 [ ]');
diff --git a/src/Task.ts b/src/Task.ts
index b009e78f..3bef98aa 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -52,8 +52,8 @@ export class TaskRegularExpressions {
     // Matches indentation before a list marker (including > for potentially nested blockquotes or Obsidian callouts)
     public static readonly indentationRegex = /^([\s\t>]*)/;
 
-    // Matches (but does not save) - or * list markers, or numbered list markers (eg 1.)
-    public static readonly listMarkerRegex = /(?:[-*]|[0-9]+\.)/;
+    // Matches - or * list markers, or numbered list markers (eg 1.)
+    public static readonly listMarkerRegex = /([-*]|[0-9]+\.)/;
 
     // Matches a checkbox and saves the status character inside
     public static readonly checkboxRegex = /\[(.)\]/u;
@@ -63,6 +63,7 @@ export class TaskRegularExpressions {
 
     // Main regex for parsing a line. It matches the following:
     // - Indentation
+    // - List marker
     // - Status character
     // - Rest of task after checkbox markdown
     public static readonly taskRegex = new RegExp(
@@ -87,7 +88,7 @@ export class TaskRegularExpressions {
 
     // Used with "Toggle Done" command to detect a list item that can get a checkbox added to it.
     public static readonly listItemRegex = new RegExp(
-        TaskRegularExpressions.indentationRegex.source + '(' + TaskRegularExpressions.listMarkerRegex.source + ')',
+        TaskRegularExpressions.indentationRegex.source + TaskRegularExpressions.listMarkerRegex.source,
     );
 
     // Match on block link at end.
@@ -262,8 +263,8 @@ export class Task {
             return null;
         }
 
-        // match[3] includes the whole body of the task after the brackets.
-        const body = regexMatch[3].trim();
+        // match[4] includes the whole body of the task after the brackets.
+        const body = regexMatch[4].trim();
 
         // return if task does not have the global filter. Do this before processing
         // rest of match to improve performance.
@@ -274,11 +275,11 @@ export class Task {
 
         let description = body;
         const indentation = regexMatch[1];
-        const listMarker = '-';
+        const listMarker = regexMatch[2];
 
         // Get the status of the task, only todo and done supported.
         // But custom ones are retained and displayed as-is.
-        const statusString = regexMatch[2];
+        const statusString = regexMatch[3];
         let status: Status;
         switch (statusString) {
             case ' ':


Test patch:
diff --git a/tests/Commands/CreateOrEditTaskParser.test.ts b/tests/Commands/CreateOrEditTaskParser.test.ts
index e087e0dc..1f7fa638 100644
--- a/tests/Commands/CreateOrEditTaskParser.test.ts
+++ b/tests/Commands/CreateOrEditTaskParser.test.ts
@@ -13,13 +13,13 @@ describe('CreateOrEditTaskParser - testing edited task if line is saved unchange
             '',
         ],
         [
-            '- [ ] #task Hello World', // Simple case, with global filter
-            '- [ ] #task Hello World',
+            '* [ ] #task Hello World - with asterisk list marker', // Simple case, with global filter - using * as list marker
+            '* [ ] #task Hello World - with asterisk list marker',
             '#task',
         ],
         [
-            '    - [ ] Hello World', // Simple case, but indented
-            '    - [ ] Hello World',
+            '    - [x] Hello World', // Completed task, indented
+            '    - [x] Hello World',
             '',
         ],
         [
@@ -32,6 +32,11 @@ describe('CreateOrEditTaskParser - testing edited task if line is saved unchange
             '- [ ] Non-blank line, not a task',
             '',
         ],
+        [
+            '* Non-blank line, not a task - with asterisk list marker',
+            '* [ ] Non-blank line, not a task - with asterisk list marker',
+            '',
+        ],
         [
             'Non-blank line, not a task', // Blank line, not yet a task - settings have global filter
             '- [ ] Non-blank line, not a task', // The global filter doesn't get added until the Modal rewrites the line
diff --git a/tests/Commands/ToggleDone.test.ts b/tests/Commands/ToggleDone.test.ts
index 990471d3..6393494e 100644
--- a/tests/Commands/ToggleDone.test.ts
+++ b/tests/Commands/ToggleDone.test.ts
@@ -4,6 +4,7 @@
 
 import moment from 'moment';
 import { calculateCursorOffset, toggleLine } from '../../src/Commands/ToggleDone';
+import { resetSettings, updateSettings } from '../../src/Config/Settings';
 
 window.moment = moment;
 
@@ -63,18 +64,34 @@ function testToggleLineForOutOfRangeCursorPositions(
 }
 
 describe('ToggleDone', () => {
+    afterEach(() => {
+        resetSettings();
+    });
+
     const todaySpy = jest.spyOn(Date, 'now').mockReturnValue(moment('2022-09-04').valueOf());
 
     // The | (pipe) indicates the calculated position where the cursor should be displayed.
     // Note that prior to the #1103 fix, this position was sometimes ignored.
 
+    // Most of the tests are run twice. The second time, they are tested with tasks that
+    // do not match the global filter.
+
     it('should add hyphen and space to empty line', () => {
         testToggleLine('|', '- |');
+
+        updateSettings({ globalFilter: '#task' });
+
+        testToggleLine('|', '- |');
     });
 
     it('should add checkbox to hyphen and space', () => {
         testToggleLine('|- ', '- [ |] ');
         testToggleLine('- |', '- [ ] |');
+
+        updateSettings({ globalFilter: '#task' });
+
+        testToggleLine('|- ', '- [ |] ');
+        testToggleLine('- |', '- [ ] |');
     });
 
     it('should complete a task', () => {
@@ -83,6 +100,14 @@ describe('ToggleDone', () => {
 
         // Issue #449 - cursor jumped 13 characters to the right on completion
         testToggleLine('- [ ] I have a |proper description', '- [x] I have a |proper description ✅ 2022-09-04');
+
+        updateSettings({ globalFilter: '#task' });
+
+        testToggleLine('|- [ ] ', '|- [x] ');
+        testToggleLine('- [ ] |', '- [x] |');
+
+        // Issue #449 - cursor jumped 13 characters to the right on completion
+        testToggleLine('- [ ] I have a |proper description', '- [x] I have a |proper description');
     });
 
     it('should un-complete a completed task', () => {
@@ -91,6 +116,18 @@ describe('ToggleDone', () => {
 
         // Issue #449 - cursor jumped 13 characters to the left on un-completion
         testToggleLine('- [x] I have a proper description| ✅ 2022-09-04', '- [ ] I have a proper description|');
+
+        updateSettings({ globalFilter: '#task' });
+
+        // Done date is not removed if task does not match global filter
+        testToggleLine('|- [x]  ✅ 2022-09-04', '|- [ ] ✅ 2022-09-04');
+        testToggleLine('- [x]  ✅ 2022-09-04|', '- [ ] ✅ 2022-09-04|');
+
+        // Issue #449 - cursor jumped 13 characters to the left on un-completion
+        testToggleLine(
+            '- [x] I have a proper description| ✅ 2022-09-04',
+            '- [ ] I have a proper description| ✅ 2022-09-04',
+        );
     });
 
     it('should complete a recurring task', () => {
@@ -107,6 +144,21 @@ describe('ToggleDone', () => {
             `- [ ] I am a recurring task 🔁 every day 📅 2022-09-05
 - [x] I am a recurring tas|k 🔁 every day 📅 2022-09-04 ✅ 2022-09-04`,
         );
+
+        updateSettings({ globalFilter: '#task' });
+
+        // Tasks do not recur, and no done-date added, if not matching global filter
+        testToggleLine(
+            '- [ ] I am a recurring task| 🔁 every day 📅 2022-09-04',
+            '- [x] I am a recurring task| 🔁 every day 📅 2022-09-04',
+        );
+
+        // With a trailing space at the end of the initial line, which is deleted
+        // when the task lines are regenerated, the cursor moves one character to the left:
+        testToggleLine(
+            '- [ ] I am a recurring task| 🔁 every day 📅 2022-09-04 ',
+            '- [x] I am a recurring task| 🔁 every day 📅 2022-09-04 ',
+        );
     });
 
     todaySpy.mockClear();
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index c449c013..584e1722 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -13,7 +13,7 @@ jest.mock('obsidian');
 window.moment = moment;
 
 describe('parsing', () => {
-    it('parses a task from a line', () => {
+    it('parses a task from a line starting with hyphen', () => {
         // Arrange
         const line = '- [x] this is a done task 🗓 2021-09-12 ✅ 2021-06-20';
 
@@ -24,6 +24,7 @@ describe('parsing', () => {
 
         // Assert
         expect(task).not.toBeNull();
+        expect(task!.listMarker).toEqual('-');
         expect(task!.description).toEqual('this is a done task');
         expect(task!.status).toStrictEqual(Status.DONE);
         expect(task!.dueDate).not.toBeNull();
@@ -33,6 +34,21 @@ describe('parsing', () => {
         expect(task!.originalMarkdown).toStrictEqual(line);
     });
 
+    it('parses a task from a line starting with asterisk', () => {
+        // Arrange
+        const line = '* [ ] this is a task in asterisk list';
+
+        // Act
+        const task = fromLine({
+            line,
+        });
+
+        // Assert
+        expect(task).not.toBeNull();
+        expect(task!.listMarker).toEqual('*');
+        expect(task!.originalMarkdown).toStrictEqual(line);
+    });
+
     it('parses a task from a line (numbered)', () => {
         // Arrange
         const line = '1. [x] this is a done task';
@@ -44,6 +60,7 @@ describe('parsing', () => {
 
         // Assert
         expect(task).not.toBeNull();
+        expect(task!.listMarker).toEqual('1.');
         expect(task!.description).toEqual('this is a done task');
         expect(task!.status).toStrictEqual(Status.DONE);
         expect(task!.originalMarkdown).toStrictEqual(line);
@@ -60,6 +77,7 @@ describe('parsing', () => {
 
         // Assert
         expect(task).not.toBeNull();
+        expect(task!.listMarker).toEqual('909999.');
         expect(task!.description).toEqual('this is a todo task');
         expect(task!.status).toStrictEqual(Status.TODO);
         expect(task!.originalMarkdown).toStrictEqual(line);