Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2752 (2024-04-06 10:54:55)

refactor: move `TasksFile` object to `TaskLocation`
<!--- Provide a general summary of your changes in the Title above -->

# Description

Done by pairing with @claremacrae 

## Motivation and Context

Preparing for https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/232#discussioncomment-8477015

## How has this been tested?

Unit tests + manual test in demo vault

## 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`.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Commands/CreateOrEditTaskParser.ts b/src/Commands/CreateOrEditTaskParser.ts
index c6759b50..a7490be6 100644
--- a/src/Commands/CreateOrEditTaskParser.ts
+++ b/src/Commands/CreateOrEditTaskParser.ts
@@ -1,3 +1,4 @@
+import { TasksFile } from '../Scripting/TasksFile';
 import { Status } from '../Statuses/Status';
 import { Task } from '../Task/Task';
 import { DateFallback } from '../Task/DateFallback';
@@ -56,7 +57,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
     // This helps users who, for some reason, have data in a task line without the Global Filter.
     const task = Task.parseTaskSignifiers(
         line,
-        TaskLocation.fromUnknownPosition(path), // We don't need precise location to toggle it here in the editor.
+        TaskLocation.fromUnknownPosition(new TasksFile(path)), // We don't need precise location to toggle it here in the editor.
         DateFallback.fromPath(path), // set the scheduled date from the filename, so it can be displayed in the dialog
     );
 
@@ -80,7 +81,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
             status: Status.TODO,
             description: '',
             // We don't need the location fields except file to edit here in the editor.
-            taskLocation: TaskLocation.fromUnknownPosition(path),
+            taskLocation: TaskLocation.fromUnknownPosition(new TasksFile(path)),
             indentation: '',
             listMarker: '-',
             priority: Priority.None,
@@ -119,7 +120,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
         status,
         description,
         // We don't need the location fields except file to edit here in the editor.
-        taskLocation: TaskLocation.fromUnknownPosition(path),
+        taskLocation: TaskLocation.fromUnknownPosition(new TasksFile(path)),
         indentation,
         listMarker,
         blockLink,
diff --git a/src/Commands/ToggleDone.ts b/src/Commands/ToggleDone.ts
index d4502a25..f7f5fa10 100644
--- a/src/Commands/ToggleDone.ts
+++ b/src/Commands/ToggleDone.ts
@@ -1,4 +1,5 @@
 import { Editor, type EditorPosition, type MarkdownFileInfo, MarkdownView } from 'obsidian';
+import { TasksFile } from '../Scripting/TasksFile';
 import { StatusRegistry } from '../Statuses/StatusRegistry';
 
 import { Task } from '../Task/Task';
@@ -69,7 +70,7 @@ export const toggleLine = (line: string, path: string): EditorInsertion => {
     const task = Task.fromLine({
         // Why are we using Task.fromLine instead of the Cache here?
         line,
-        taskLocation: TaskLocation.fromUnknownPosition(path), // We don't need precise location to toggle it here in the editor.
+        taskLocation: TaskLocation.fromUnknownPosition(new TasksFile(path)), // We don't need precise location to toggle it here in the editor.
         fallbackDate: null, // We don't need this to toggle it here in the editor.
     });
     if (task !== null) {
diff --git a/src/Obsidian/Cache.ts b/src/Obsidian/Cache.ts
index 3b8b345b..1e3ffc0b 100644
--- a/src/Obsidian/Cache.ts
+++ b/src/Obsidian/Cache.ts
@@ -2,6 +2,7 @@ import { MetadataCache, Notice, TAbstractFile, TFile, Vault } from 'obsidian';
 import type { CachedMetadata, EventRef } from 'obsidian';
 import type { HeadingCache, ListItemCache, SectionCache } from 'obsidian';
 import { Mutex } from 'async-mutex';
+import { TasksFile } from '../Scripting/TasksFile';
 
 import { Task } from '../Task/Task';
 import { DateFallback } from '../Task/DateFallback';
@@ -158,6 +159,7 @@ export class Cache {
             this.logger.debug(`Cache.subscribeToVault.renamedEventReference() ${file.path}`);
 
             this.tasksMutex.runExclusive(() => {
+                const tasksFile = new TasksFile(file.path);
                 const fallbackDate = new Lazy(() => DateFallback.fromPath(file.path));
 
                 this.tasks = this.tasks.map((task: Task): Task => {
@@ -165,7 +167,7 @@ export class Cache {
                         if (!useFilenameAsScheduledDate) {
                             return new Task({
                                 ...task,
-                                taskLocation: task.taskLocation.fromRenamedFile(file.path),
+                                taskLocation: task.taskLocation.fromRenamedFile(tasksFile),
                             });
                         } else {
                             return DateFallback.updateTaskPath(task, file.path, fallbackDate.value);
@@ -277,6 +279,7 @@ export class Cache {
         fileCache: CachedMetadata,
         file: TFile,
     ): Task[] {
+        const tasksFile = new TasksFile(file.path);
         const tasks: Task[] = [];
         const fileLines = fileContent.split('\n');
         const linesInFile = fileLines.length;
@@ -333,7 +336,7 @@ export class Cache {
                     task = Task.fromLine({
                         line,
                         taskLocation: new TaskLocation(
-                            file.path,
+                            tasksFile,
                             lineNumber,
                             currentSection.position.start.line,
                             sectionIndex,
diff --git a/src/Obsidian/InlineRenderer.ts b/src/Obsidian/InlineRenderer.ts
index 143d4c44..1b772050 100644
--- a/src/Obsidian/InlineRenderer.ts
+++ b/src/Obsidian/InlineRenderer.ts
@@ -3,6 +3,7 @@ import { MarkdownRenderChild } from 'obsidian';
 import { GlobalFilter } from '../Config/GlobalFilter';
 import { TaskLayoutOptions } from '../Layout/TaskLayoutOptions';
 import { QueryLayoutOptions } from '../Layout/QueryLayoutOptions';
+import { TasksFile } from '../Scripting/TasksFile';
 import { Task } from '../Task/Task';
 import { TaskLineRenderer } from '../Renderer/TaskLineRenderer';
 import { TaskLocation } from '../Task/TaskLocation';
@@ -79,7 +80,13 @@ export class InlineRenderer {
             const precedingHeader = null; // We don't need the preceding header for in-line rendering.
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(path, lineNumber, section.lineStart, sectionIndex, precedingHeader),
+                taskLocation: new TaskLocation(
+                    new TasksFile(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/Obsidian/LivePreviewExtension.ts b/src/Obsidian/LivePreviewExtension.ts
index 043264cf..a75d95d7 100644
--- a/src/Obsidian/LivePreviewExtension.ts
+++ b/src/Obsidian/LivePreviewExtension.ts
@@ -1,6 +1,7 @@
 import { EditorView, ViewPlugin } from '@codemirror/view';
 import type { PluginValue } from '@codemirror/view';
 import { Notice } from 'obsidian';
+import { TasksFile } from '../Scripting/TasksFile';
 
 import { Task } from '../Task/Task';
 import { TaskLocation } from '../Task/TaskLocation';
@@ -60,7 +61,7 @@ class LivePreviewExtension implements PluginValue {
             // None of this data is relevant here.
             // The task is created, toggled, and written back to the CM6 document,
             // replacing the old task in-place.
-            taskLocation: TaskLocation.fromUnknownPosition(''),
+            taskLocation: TaskLocation.fromUnknownPosition(new TasksFile('')),
             fallbackDate: null,
         });
 
diff --git a/src/Task/DateFallback.ts b/src/Task/DateFallback.ts
index ee4c295b..841f95fe 100644
--- a/src/Task/DateFallback.ts
+++ b/src/Task/DateFallback.ts
@@ -1,5 +1,6 @@
 import type { Moment } from 'moment';
 import { getSettings } from '../Config/Settings';
+import { TasksFile } from '../Scripting/TasksFile';
 import { Task } from './Task';
 
 /**
@@ -114,7 +115,7 @@ export class DateFallback {
 
         return new Task({
             ...task,
-            taskLocation: task.taskLocation.fromRenamedFile(newPath),
+            taskLocation: task.taskLocation.fromRenamedFile(new TasksFile(newPath)),
             scheduledDate,
             scheduledDateIsInferred,
         });
diff --git a/src/Task/Task.ts b/src/Task/Task.ts
index 8d1769c3..f4b270e3 100644
--- a/src/Task/Task.ts
+++ b/src/Task/Task.ts
@@ -6,7 +6,7 @@ import type { Status } from '../Statuses/Status';
 import { compareByDate } from '../lib/DateTools';
 import { TasksDate } from '../Scripting/TasksDate';
 import { StatusType } from '../Statuses/StatusConfiguration';
-import { TasksFile } from '../Scripting/TasksFile';
+import type { TasksFile } from '../Scripting/TasksFile';
 import { PriorityTools } from '../lib/PriorityTools';
 import { logging } from '../lib/logging';
 import { logEndOfTaskEdit, logStartOfTaskEdit } from '../lib/LogTasksHelper';
@@ -728,7 +728,7 @@ export class Task {
     }
 
     public get file(): TasksFile {
-        return new TasksFile(this.path);
+        return this.taskLocation.tasksFile;
     }
 
     /**
diff --git a/src/Task/TaskLocation.ts b/src/Task/TaskLocation.ts
index d1f66470..6e66388d 100644
--- a/src/Task/TaskLocation.ts
+++ b/src/Task/TaskLocation.ts
@@ -1,22 +1,24 @@
+import type { TasksFile } from '../Scripting/TasksFile';
+
 /**
  * TaskLocation is the place where all information about a task line's location
  * in a markdown file is stored, so that testable algorithms can then be added here.
  */
 export class TaskLocation {
-    private readonly _path: string;
+    private readonly _tasksFile: TasksFile;
     private readonly _lineNumber: number;
     private readonly _sectionStart: number;
     private readonly _sectionIndex: number;
     private readonly _precedingHeader: string | null;
 
     public constructor(
-        path: string,
+        tasksFile: TasksFile,
         lineNumber: number,
         sectionStart: number,
         sectionIndex: number,
         precedingHeader: string | null,
     ) {
-        this._path = path;
+        this._tasksFile = tasksFile;
         this._lineNumber = lineNumber;
         this._sectionStart = sectionStart;
         this._sectionIndex = sectionIndex;
@@ -25,22 +27,32 @@ export class TaskLocation {
 
     /**
      * Constructor, for use when the Task's exact location in a file is either unknown, or not needed.
-     * @param path
+     * @param tasksFile
      */
-    public static fromUnknownPosition(path: string): TaskLocation {
-        return new TaskLocation(path, 0, 0, 0, null);
+    public static fromUnknownPosition(tasksFile: TasksFile): TaskLocation {
+        return new TaskLocation(tasksFile, 0, 0, 0, null);
     }
 
     /**
      * Constructor, for when the file has been renamed, and all other data remains the same.
-     * @param newPath
+     * @param newTasksFile
      */
-    fromRenamedFile(newPath: string) {
-        return new TaskLocation(newPath, this.lineNumber, this.sectionStart, this.sectionIndex, this.precedingHeader);
+    fromRenamedFile(newTasksFile: TasksFile) {
+        return new TaskLocation(
+            newTasksFile,
+            this.lineNumber,
+            this.sectionStart,
+            this.sectionIndex,
+            this.precedingHeader,
+        );
+    }
+
+    public get tasksFile(): TasksFile {
+        return this._tasksFile;
     }
 
     public get path(): string {
-        return this._path;
+        return this._tasksFile.path;
     }
 
     public get lineNumber(): number {


Test patch:
diff --git a/tests/Query/Query.test.ts b/tests/Query/Query.test.ts
index 65e4876c..75226e16 100644
--- a/tests/Query/Query.test.ts
+++ b/tests/Query/Query.test.ts
@@ -3,6 +3,7 @@
  */
 import moment from 'moment';
 import { Query } from '../../src/Query/Query';
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { Status } from '../../src/Statuses/Status';
 import { Task } from '../../src/Task/Task';
 import { TaskLocation } from '../../src/Task/TaskLocation';
@@ -672,7 +673,7 @@ describe('Query', () => {
                 new Task({
                     status: Status.TODO,
                     description: 'description',
-                    taskLocation: TaskLocation.fromUnknownPosition('Ab/C D'),
+                    taskLocation: TaskLocation.fromUnknownPosition(new TasksFile('Ab/C D')),
                     indentation: '',
                     listMarker: '-',
                     priority: Priority.None,
@@ -693,7 +694,7 @@ describe('Query', () => {
                 new Task({
                     status: Status.TODO,
                     description: 'description',
-                    taskLocation: TaskLocation.fromUnknownPosition('FF/C D'),
+                    taskLocation: TaskLocation.fromUnknownPosition(new TasksFile('FF/C D')),
                     indentation: '',
                     listMarker: '-',
                     priority: Priority.None,
diff --git a/tests/Statuses/StatusRegistry.test.ts b/tests/Statuses/StatusRegistry.test.ts
index 08ef825f..6a8a341b 100644
--- a/tests/Statuses/StatusRegistry.test.ts
+++ b/tests/Statuses/StatusRegistry.test.ts
@@ -2,6 +2,7 @@
  * @jest-environment jsdom
  */
 import moment from 'moment';
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { StatusRegistry } from '../../src/Statuses/StatusRegistry';
 import { Status } from '../../src/Statuses/Status';
 import { StatusConfiguration, StatusType } from '../../src/Statuses/StatusConfiguration';
@@ -370,7 +371,13 @@ describe('StatusRegistry', () => {
         const sectionStart = 1337;
         const sectionIndex = 1209;
         const precedingHeader = 'Eloquent Section';
-        const taskLocation = new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader);
+        const taskLocation = new TaskLocation(
+            new TasksFile(path),
+            lineNumber,
+            sectionStart,
+            sectionIndex,
+            precedingHeader,
+        );
         const fallbackDate = null;
 
         it('should allow task to toggle through standard transitions', () => {
diff --git a/tests/Task/DateFallback.test.ts b/tests/Task/DateFallback.test.ts
index d1c64156..a096cd79 100644
--- a/tests/Task/DateFallback.test.ts
+++ b/tests/Task/DateFallback.test.ts
@@ -3,6 +3,7 @@
  */
 import type { Moment } from 'moment';
 import moment from 'moment';
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { Task } from '../../src/Task/Task';
 import { resetSettings, updateSettings } from '../../src/Config/Settings';
 import { DateFallback } from '../../src/Task/DateFallback';
@@ -185,7 +186,7 @@ describe('extract date from filename', () => {
 function constructTaskFromLine(line: string, fallbackDate: string | null) {
     return Task.fromLine({
         line,
-        taskLocation: TaskLocation.fromUnknownPosition('file.md'), // filename must be parsed before calling Task.fromLine, so irrelevant for these tests
+        taskLocation: TaskLocation.fromUnknownPosition(new TasksFile('file.md')), // filename must be parsed before calling Task.fromLine, so irrelevant for these tests
         fallbackDate: date(fallbackDate),
     });
 }
diff --git a/tests/Task/Task.test.ts b/tests/Task/Task.test.ts
index 16580a98..de129142 100644
--- a/tests/Task/Task.test.ts
+++ b/tests/Task/Task.test.ts
@@ -3,6 +3,7 @@
  */
 import moment from 'moment';
 import { verifyAll } from 'approvals/lib/Providers/Jest/JestApprovals';
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { Status } from '../../src/Statuses/Status';
 import { Task } from '../../src/Task/Task';
 import { resetSettings, updateSettings } from '../../src/Config/Settings';
@@ -422,7 +423,7 @@ describe('parsing tags', () => {
             // Act
             const task = Task.fromLine({
                 line: markdownTask,
-                taskLocation: TaskLocation.fromUnknownPosition('file.md'),
+                taskLocation: TaskLocation.fromUnknownPosition(new TasksFile('file.md')),
                 fallbackDate: null,
             });
 
diff --git a/tests/Task/TaskLocation.test.ts b/tests/Task/TaskLocation.test.ts
index d1c747b9..abdf09ee 100644
--- a/tests/Task/TaskLocation.test.ts
+++ b/tests/Task/TaskLocation.test.ts
@@ -1,3 +1,4 @@
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { TaskLocation } from '../../src/Task/TaskLocation';
 
 describe('TaskLocation', () => {
@@ -10,10 +11,17 @@ describe('TaskLocation', () => {
         const precedingHeader = 'My Header';
 
         // Act
-        const taskLocation = new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader);
+        const taskLocation = new TaskLocation(
+            new TasksFile(path),
+            lineNumber,
+            sectionStart,
+            sectionIndex,
+            precedingHeader,
+        );
 
         // Assert
         expect(taskLocation.path).toStrictEqual(path);
+        expect(taskLocation.tasksFile.path).toStrictEqual(path);
         expect(taskLocation.lineNumber).toStrictEqual(lineNumber);
         expect(taskLocation.sectionStart).toStrictEqual(sectionStart);
         expect(taskLocation.sectionIndex).toStrictEqual(sectionIndex);
@@ -25,7 +33,7 @@ describe('TaskLocation', () => {
         const path = 'a/b/c.md';
 
         // Act
-        const taskLocation = TaskLocation.fromUnknownPosition(path);
+        const taskLocation = TaskLocation.fromUnknownPosition(new TasksFile(path));
 
         // Assert
         expect(taskLocation.path).toStrictEqual(path);
@@ -42,11 +50,17 @@ describe('TaskLocation', () => {
         const sectionStart = 13;
         const sectionIndex = 10;
         const precedingHeader = 'My Previous Header';
-        const taskLocation = new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader);
+        const taskLocation = new TaskLocation(
+            new TasksFile(path),
+            lineNumber,
+            sectionStart,
+            sectionIndex,
+            precedingHeader,
+        );
 
         // Act
         const newPath = 'd/e/f.md';
-        const newLocation = taskLocation.fromRenamedFile(newPath);
+        const newLocation = taskLocation.fromRenamedFile(new TasksFile(newPath));
 
         // Assert
         expect(newLocation.path).toStrictEqual(newPath);
@@ -57,7 +71,7 @@ describe('TaskLocation', () => {
     });
 
     it('should recognise unknown paths', () => {
-        expect(TaskLocation.fromUnknownPosition('x.md').hasKnownPath).toBe(true);
-        expect(TaskLocation.fromUnknownPosition('').hasKnownPath).toBe(false);
+        expect(TaskLocation.fromUnknownPosition(new TasksFile('x.md')).hasKnownPath).toBe(true);
+        expect(TaskLocation.fromUnknownPosition(new TasksFile('')).hasKnownPath).toBe(false);
     });
 });
diff --git a/tests/TestingTools/FilterTestHelpers.ts b/tests/TestingTools/FilterTestHelpers.ts
index 43bb6bd5..82719841 100644
--- a/tests/TestingTools/FilterTestHelpers.ts
+++ b/tests/TestingTools/FilterTestHelpers.ts
@@ -1,4 +1,5 @@
 import type { FilterOrErrorMessage } from '../../src/Query/Filter/FilterOrErrorMessage';
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { Task } from '../../src/Task/Task';
 import { Query } from '../../src/Query/Query';
 import { TaskLocation } from '../../src/Task/TaskLocation';
@@ -78,7 +79,7 @@ export function shouldSupportFiltering(
         (taskLine) =>
             Task.fromLine({
                 line: taskLine,
-                taskLocation: TaskLocation.fromUnknownPosition(''),
+                taskLocation: TaskLocation.fromUnknownPosition(new TasksFile('')),
                 fallbackDate: null, // For tests scheduled date needs to be set explicitly
             }) as Task,
     );
diff --git a/tests/TestingTools/TaskBuilder.ts b/tests/TestingTools/TaskBuilder.ts
index 98c81ff9..a8fa90c3 100644
--- a/tests/TestingTools/TaskBuilder.ts
+++ b/tests/TestingTools/TaskBuilder.ts
@@ -1,5 +1,6 @@
 // Builder
 import type { Moment } from 'moment';
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { Status } from '../../src/Statuses/Status';
 import { Task } from '../../src/Task/Task';
 import { Recurrence } from '../../src/Task/Recurrence';
@@ -72,7 +73,7 @@ export class TaskBuilder {
             status: this._status,
             description: description,
             taskLocation: new TaskLocation(
-                this._path,
+                new TasksFile(this._path),
                 this._lineNumber,
                 this._sectionStart,
                 this._sectionIndex,
diff --git a/tests/TestingTools/TestHelpers.ts b/tests/TestingTools/TestHelpers.ts
index 059a49f0..7ad22699 100644
--- a/tests/TestingTools/TestHelpers.ts
+++ b/tests/TestingTools/TestHelpers.ts
@@ -1,3 +1,4 @@
+import { TasksFile } from '../../src/Scripting/TasksFile';
 import { Task } from '../../src/Task/Task';
 import { TaskLocation } from '../../src/Task/TaskLocation';
 
@@ -16,7 +17,7 @@ export function fromLine({
 }) {
     return Task.fromLine({
         line,
-        taskLocation: new TaskLocation(path, 0, 0, 0, precedingHeader),
+        taskLocation: new TaskLocation(new TasksFile(path), 0, 0, 0, precedingHeader),
         fallbackDate: null,
     })!;
 }
@@ -71,7 +72,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, 0, precedingHeader),
+            taskLocation: new TaskLocation(new TasksFile(path), 0, 0, 0, precedingHeader),
             fallbackDate: null,
         });
         if (task) {