Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1700 (2023-02-27 09:32:23)

refactor: Store task line numbers
<!--- Provide a general summary of your changes in the Title above -->

# Description

Add line number to `TaskLocation`, and update the debug rendering of task data to display it.

## Motivation and Context

This is a step on the way to removing the fragile and hard-to-reason-about  `task.sectionStart` and `task.sectionIndex`.

## How has this been tested?

- Running existing tests
- Adding tests
- Testing in the demo vault

## Screenshots (if appropriate)

This is what the 2 lines of debug display look like now, where `null` is the heading not specified for rendering in Reading mode.

![image](https://user-images.githubusercontent.com/4840096/221526253-57131020-ff90-493f-94ed-9fa4594b4ef1.png)


## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

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)

## 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://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/CONTRIBUTING.md b/CONTRIBUTING.md
index e5029378..dba0ebf6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -40,6 +40,7 @@
   - [Notes and Special Cases](#notes-and-special-cases)
 - [FAQs](#faqs)
   - [How does Tasks handle status changes?](#how-does-tasks-handle-status-changes)
+  - [How do I enable hidden debugging/visualisation facilities?](#how-do-i-enable-hidden-debuggingvisualisation-facilities)
   - [How do I add a new field to the Task class?](#how-do-i-add-a-new-field-to-the-task-class)
   - [How do I add a new task filter?](#how-do-i-add-a-new-task-filter)
     - [Update src/](#update-src)
@@ -523,15 +524,15 @@ This is what these options do:
   - This adjusts the rendering of Task objects to display some extra information, to make the plugin's behaviour easier to inspect.
   - The values display are:
     - Line 1:
+      - `task.lineNumber`
       - `task.sectionStart`
       - `task.sectionIndex`
-      - `task.precedingHeader`
-      - `task.path`
-    - Line 2:
       - `task.originalMarkdown`
-  - Here is an example of the extra output:
-  🐛 **4** . 6 . 'Steps to world domination' . 'ACME.md'
-  '`- [ ] #task Feed the baby 🔽 📅 2021-11-21`'
+    - Line 2:
+      - `task.path`
+      - `task.precedingHeader`
+  - Here is an example of the extra output:<br>
+  🐛 **11** . 4 . 6 . '`- [ ] #task Feed the baby 🔽 📅 2021-11-21`'<br>'`a/b/c.d`' > '`Previous Heading`'<br>
 
 ### How do I add a new field to the Task class?
 
diff --git a/src/Cache.ts b/src/Cache.ts
index 5cf5f2a1..4930ea55 100644
--- a/src/Cache.ts
+++ b/src/Cache.ts
@@ -300,6 +300,7 @@ export class Cache {
                         line,
                         taskLocation: new TaskLocation(
                             file.path,
+                            lineNumber,
                             currentSection.position.start.line,
                             sectionIndex,
                             Cache.getPrecedingHeader(lineNumber, fileCache.headings),
diff --git a/src/InlineRenderer.ts b/src/InlineRenderer.ts
index 6dd0d8b9..977f1941 100644
--- a/src/InlineRenderer.ts
+++ b/src/InlineRenderer.ts
@@ -65,14 +65,10 @@ export class InlineRenderer {
                 continue;
             }
 
+            const precedingHeader = null; // We don't need the preceding header for in-line rendering.
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(
-                    path,
-                    section.lineStart,
-                    sectionIndex,
-                    null, // We don't need the preceding header for in-line rendering.
-                ),
+                taskLocation: new TaskLocation(path, lineNumber, section.lineStart, sectionIndex, precedingHeader),
                 fallbackDate: null, // We don't need the fallback date for in-line rendering
             });
             if (task !== null) {
diff --git a/src/Task.ts b/src/Task.ts
index 7c31cdb6..5d6b2963 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -564,6 +564,10 @@ export class Task {
         }
     }
 
+    get lineNumber(): number {
+        return this.taskLocation.lineNumber;
+    }
+
     get sectionStart(): number {
         return this.taskLocation.sectionStart;
     }
@@ -649,6 +653,7 @@ export class Task {
             'path',
             'indentation',
             'listMarker',
+            'lineNumber',
             'sectionStart',
             'sectionIndex',
             'precedingHeader',
diff --git a/src/TaskLineRenderer.ts b/src/TaskLineRenderer.ts
index 89844bd8..45e4846f 100644
--- a/src/TaskLineRenderer.ts
+++ b/src/TaskLineRenderer.ts
@@ -112,7 +112,7 @@ async function taskToHtml(
     const { debugSettings } = getSettings();
     if (debugSettings.showTaskHiddenData) {
         // Add some debug output to enable hidden information in the task to be inspected.
-        taskAsString += `<br>🐛 <b>${task.sectionStart}</b> . ${task.sectionIndex} . '${task.precedingHeader}' . '${task.path}'<br>'<code>${task.originalMarkdown}</code>'<br>`;
+        taskAsString += `<br>🐛 <b>${task.lineNumber}</b> . ${task.sectionStart} . ${task.sectionIndex} . '<code>${task.originalMarkdown}</code>'<br>'<code>${task.path}</code>' > '<code>${task.precedingHeader}</code>'<br>`;
     }
 
     await renderComponentText(parentElement, taskAsString, 'description', task, textRenderer);
diff --git a/src/TaskLocation.ts b/src/TaskLocation.ts
index 9a3d3302..822cc504 100644
--- a/src/TaskLocation.ts
+++ b/src/TaskLocation.ts
@@ -4,12 +4,20 @@
  */
 export class TaskLocation {
     private readonly _path: string;
+    private readonly _lineNumber: number;
     private readonly _sectionStart: number;
     private readonly _sectionIndex: number;
     private readonly _precedingHeader: string | null;
 
-    public constructor(path: string, sectionStart: number, sectionIndex: number, precedingHeader: string | null) {
+    public constructor(
+        path: string,
+        lineNumber: number,
+        sectionStart: number,
+        sectionIndex: number,
+        precedingHeader: string | null,
+    ) {
         this._path = path;
+        this._lineNumber = lineNumber;
         this._sectionStart = sectionStart;
         this._sectionIndex = sectionIndex;
         this._precedingHeader = precedingHeader;
@@ -20,7 +28,7 @@ export class TaskLocation {
      * @param path
      */
     public static fromUnknownPosition(path: string): TaskLocation {
-        return new TaskLocation(path, 0, 0, null);
+        return new TaskLocation(path, 0, 0, 0, null);
     }
 
     /**
@@ -28,13 +36,17 @@ export class TaskLocation {
      * @param newPath
      */
     fromRenamedFile(newPath: string) {
-        return new TaskLocation(newPath, this.sectionStart, this.sectionIndex, this.precedingHeader);
+        return new TaskLocation(newPath, this.lineNumber, this.sectionStart, this.sectionIndex, this.precedingHeader);
     }
 
     public get path(): string {
         return this._path;
     }
 
+    public get lineNumber(): number {
+        return this._lineNumber;
+    }
+
     /** Line number where the section starts that contains this task. */
     get sectionStart(): number {
         return this._sectionStart;


Test patch:
diff --git a/tests/StatusRegistry.test.ts b/tests/StatusRegistry.test.ts
index 20dd82dc..250b21bd 100644
--- a/tests/StatusRegistry.test.ts
+++ b/tests/StatusRegistry.test.ts
@@ -153,12 +153,13 @@ describe('StatusRegistry', () => {
             // in beforeEach() above.
             const line = '- [ ] this is a task starting at A';
             const path = 'file.md';
+            const lineNumber = 3456;
             const sectionStart = 1337;
             const sectionIndex = 1209;
             const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(path, sectionStart, sectionIndex, precedingHeader),
+                taskLocation: new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader),
                 fallbackDate: null,
             });
 
@@ -181,12 +182,13 @@ describe('StatusRegistry', () => {
             // in beforeEach() above.
             const line = '- [-] This is a cancelled task';
             const path = 'file.md';
+            const lineNumber = 3456;
             const sectionStart = 1337;
             const sectionIndex = 1209;
             const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(path, sectionStart, sectionIndex, precedingHeader),
+                taskLocation: new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader),
                 fallbackDate: null,
             });
 
@@ -215,12 +217,13 @@ describe('StatusRegistry', () => {
             globalStatusRegistry.add(statusD);
             const line = '- [a] this is a task starting at A';
             const path = 'file.md';
+            const lineNumber = 3456;
             const sectionStart = 1337;
             const sectionIndex = 1209;
             const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(path, sectionStart, sectionIndex, precedingHeader),
+                taskLocation: new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader),
                 fallbackDate: null,
             });
 
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index c9432c66..8fb0c093 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -36,6 +36,7 @@ describe('parsing', () => {
         expect(task!.doneDate).not.toBeNull();
         expect(task!.doneDate!.isSame(moment('2021-06-20', 'YYYY-MM-DD'))).toStrictEqual(true);
         expect(task!.originalMarkdown).toStrictEqual(line);
+        expect(task!.lineNumber).toEqual(0);
     });
 
     it('parses a task from a line starting with asterisk', () => {
@@ -994,6 +995,12 @@ describe('identicalTo', () => {
         expect(lhs).not.toBeIdenticalTo(new TaskBuilder().listMarker('-'));
     });
 
+    it('should check lineNumber', () => {
+        const lhs = new TaskBuilder().lineNumber(0);
+        expect(lhs).toBeIdenticalTo(new TaskBuilder().lineNumber(0));
+        expect(lhs).not.toBeIdenticalTo(new TaskBuilder().lineNumber(2));
+    });
+
     it('should check sectionStart', () => {
         const lhs = new TaskBuilder().sectionStart(0);
         expect(lhs).toBeIdenticalTo(new TaskBuilder().sectionStart(0));
diff --git a/tests/TaskLineRenderer.test.ts b/tests/TaskLineRenderer.test.ts
index eb7b8adf..42af0bf0 100644
--- a/tests/TaskLineRenderer.test.ts
+++ b/tests/TaskLineRenderer.test.ts
@@ -189,7 +189,7 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Task with invalid due date 📅 2023-11-02',
             {},
-            "Task with invalid due date 📅 2023-11-02<br>🐛 <b>0</b> . 0 . 'Previous Heading' . 'a/b/c.d'<br>'<code>- [ ] Task with invalid due date 📅 2023-11-02</code>'<br>",
+            "Task with invalid due date 📅 2023-11-02<br>🐛 <b>0</b> . 0 . 0 . '<code>- [ ] Task with invalid due date 📅 2023-11-02</code>'<br>'<code>a/b/c.d</code>' > '<code>Previous Heading</code>'<br>",
         );
     });
 
diff --git a/tests/TaskLocation.test.ts b/tests/TaskLocation.test.ts
index 4401acbe..e9dd1346 100644
--- a/tests/TaskLocation.test.ts
+++ b/tests/TaskLocation.test.ts
@@ -4,15 +4,17 @@ describe('TaskLocation', () => {
     it('should store the full task location', () => {
         // Arrange
         const path = 'a/b/c.md';
+        const lineNumber = 15;
         const sectionStart = 3;
         const sectionIndex = 7;
         const precedingHeader = 'My Header';
 
         // Act
-        const taskLocation = new TaskLocation(path, sectionStart, sectionIndex, precedingHeader);
+        const taskLocation = new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader);
 
         // Assert
         expect(taskLocation.path).toStrictEqual(path);
+        expect(taskLocation.lineNumber).toStrictEqual(lineNumber);
         expect(taskLocation.sectionStart).toStrictEqual(sectionStart);
         expect(taskLocation.sectionIndex).toStrictEqual(sectionIndex);
         expect(taskLocation.precedingHeader).toStrictEqual(precedingHeader);
@@ -27,6 +29,7 @@ describe('TaskLocation', () => {
 
         // Assert
         expect(taskLocation.path).toStrictEqual(path);
+        expect(taskLocation.lineNumber).toStrictEqual(0);
         expect(taskLocation.sectionStart).toStrictEqual(0);
         expect(taskLocation.sectionIndex).toStrictEqual(0);
         expect(taskLocation.precedingHeader).toBeNull();
@@ -35,10 +38,11 @@ describe('TaskLocation', () => {
     it('should provide convenient renaming of path', () => {
         // Arrange
         const path = 'a/b/c.md';
+        const lineNumber = 43;
         const sectionStart = 13;
         const sectionIndex = 10;
         const precedingHeader = 'My Previous Header';
-        const taskLocation = new TaskLocation(path, sectionStart, sectionIndex, precedingHeader);
+        const taskLocation = new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader);
 
         // Act
         const newPath = 'd/e/f.md';
@@ -46,6 +50,7 @@ describe('TaskLocation', () => {
 
         // Assert
         expect(newLocation.path).toStrictEqual(newPath);
+        expect(newLocation.lineNumber).toStrictEqual(lineNumber);
         expect(newLocation.sectionStart).toStrictEqual(sectionStart);
         expect(newLocation.sectionIndex).toStrictEqual(sectionIndex);
         expect(newLocation.precedingHeader).toStrictEqual(precedingHeader);
diff --git a/tests/TestHelpers.ts b/tests/TestHelpers.ts
index 5bc13537..bbe4ebf7 100644
--- a/tests/TestHelpers.ts
+++ b/tests/TestHelpers.ts
@@ -12,7 +12,7 @@ export function fromLine({
 }) {
     return Task.fromLine({
         line,
-        taskLocation: new TaskLocation(path, 0, 0, precedingHeader),
+        taskLocation: new TaskLocation(path, 0, 0, 0, precedingHeader),
         fallbackDate: null,
     })!;
 }
@@ -23,7 +23,7 @@ export function createTasksFromMarkdown(tasksAsMarkdown: string, path: string, p
     for (const line of taskLines) {
         const task = Task.fromLine({
             line: line,
-            taskLocation: new TaskLocation(path, 0, 0, precedingHeader),
+            taskLocation: new TaskLocation(path, 0, 0, 0, precedingHeader),
             fallbackDate: null,
         });
         if (task) {
diff --git a/tests/TestingTools/TaskBuilder.ts b/tests/TestingTools/TaskBuilder.ts
index cabe9306..72e2bfb2 100644
--- a/tests/TestingTools/TaskBuilder.ts
+++ b/tests/TestingTools/TaskBuilder.ts
@@ -26,6 +26,7 @@ export class TaskBuilder {
     private _indentation: string = '';
     private _listMarker: string = '-';
 
+    private _lineNumber: number = 0;
     private _sectionStart: number = 0;
     private _sectionIndex: number = 0;
 
@@ -63,7 +64,13 @@ export class TaskBuilder {
         return new Task({
             status: this._status,
             description: description,
-            taskLocation: new TaskLocation(this._path, this._sectionStart, this._sectionIndex, this._precedingHeader),
+            taskLocation: new TaskLocation(
+                this._path,
+                this._lineNumber,
+                this._sectionStart,
+                this._sectionIndex,
+                this._precedingHeader,
+            ),
             indentation: this._indentation,
             listMarker: this._listMarker,
             priority: this._priority,
@@ -130,6 +137,11 @@ export class TaskBuilder {
         return this;
     }
 
+    public lineNumber(lineNumber: number): TaskBuilder {
+        this._lineNumber = lineNumber;
+        return this;
+    }
+
     public sectionStart(sectionStart: number): TaskBuilder {
         this._sectionStart = sectionStart;
         return this;