Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2875 (2024-06-03 12:33:05)

refactor: Store `ListItems`
# Types of changes

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)

## Description

- store list item objects when reading .md files

## Motivation and Context

Store list items and tasks hierarchy

## How has this been tested?

Unit tests

## Checklist

- [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

- [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/Obsidian/Cache.ts b/src/Obsidian/Cache.ts
index aaf4bd39..def12b17 100644
--- a/src/Obsidian/Cache.ts
+++ b/src/Obsidian/Cache.ts
@@ -3,6 +3,7 @@ import type { CachedMetadata, EventRef } from 'obsidian';
 import type { HeadingCache, ListItemCache, SectionCache } from 'obsidian';
 import { Mutex } from 'async-mutex';
 import { TasksFile } from '../Scripting/TasksFile';
+import { ListItem } from '../Task/ListItem';
 
 import { Task } from '../Task/Task';
 import { DateFallback } from '../Task/DateFallback';
@@ -40,7 +41,7 @@ export function getTasksFromFileContent2(
     // rendered lists.
     let currentSection: SectionCache | null = null;
     let sectionIndex = 0;
-    const line2Task: Map<number, Task> = new Map();
+    const line2ListItem: Map<number, ListItem> = new Map();
     for (const listItem of listItems) {
         if (listItem.task !== undefined) {
             const lineNumber = listItem.position.start.line;
@@ -95,16 +96,16 @@ export function getTasksFromFileContent2(
 
                 if (task !== null) {
                     // listItem.parent could be negative if the parent is not found (in other words, it is a root task).
-                    // That is not a problem, as we never put a negative number in line2Task map, so parent will be null.
-                    const parentTask: Task | null = line2Task.get(listItem.parent) ?? null;
-                    if (parentTask !== null) {
+                    // That is not a problem, as we never put a negative number in line2ListItem map, so parent will be null.
+                    const parentListItem: ListItem | null = line2ListItem.get(listItem.parent) ?? null;
+                    if (parentListItem !== null) {
                         task = new Task({
                             ...task,
-                            parent: parentTask,
+                            parent: parentListItem,
                         });
                     }
 
-                    line2Task.set(lineNumber, task);
+                    line2ListItem.set(lineNumber, task);
                 }
             } catch (e) {
                 errorReporter(e, filePath, listItem, line);
@@ -115,6 +116,21 @@ export function getTasksFromFileContent2(
                 sectionIndex++;
                 tasks.push(task);
             }
+        } else {
+            // 1st:
+            // Root ListItems should not be parents of anything.
+            // This behavior was introduced in DataView plugin, so we want keep it for consistency reasons.
+            // 2nd:
+            // Usually, root listItem has parent = -lineNumber, but for listItems on the top of the file (on 0 line), it is -1.
+            if (listItem.parent < 0) {
+                continue;
+            }
+
+            const lineNumber = listItem.position.start.line;
+
+            const parentListItem: ListItem | null = line2ListItem.get(listItem.parent) ?? null;
+
+            line2ListItem.set(lineNumber, new ListItem(fileLines[lineNumber], parentListItem));
         }
     }
 


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_listitem_task.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_listitem_task.md
new file mode 100644
index 00000000..04092199
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_listitem_task.md
@@ -0,0 +1,2 @@
+- parent list item
+    - [ ] child task
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_listitem_task_siblings.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_listitem_task_siblings.md
new file mode 100644
index 00000000..65e11e66
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_listitem_task_siblings.md
@@ -0,0 +1,2 @@
+- list item
+- [ ] task
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_2listitem_3task.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_2listitem_3task.md
new file mode 100644
index 00000000..6485bec7
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_2listitem_3task.md
@@ -0,0 +1,6 @@
+- [ ] parent task
+    - child list item 1
+        - [ ] grandchild task 1
+        - [ ] grandchild task 2
+    - child list item 2
+        - [ ] grandchild task 3
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem.md
new file mode 100644
index 00000000..0b10f6df
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem.md
@@ -0,0 +1,2 @@
+- [ ] parent task
+    - child list item
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem_mixed_grandchildren.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem_mixed_grandchildren.md
new file mode 100644
index 00000000..9ba64837
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem_mixed_grandchildren.md
@@ -0,0 +1,5 @@
+- [ ] parent task
+    - child list item
+        - grandchild list item 1
+        - [ ] grandchild task
+        - grandchild list item 2
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem_task.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem_task.md
new file mode 100644
index 00000000..14d393a6
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_listitem_task.md
@@ -0,0 +1,3 @@
+- [ ] parent task
+    - child list item
+        - [ ] grandchild task
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_mixed_children.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_mixed_children.md
new file mode 100644
index 00000000..edd400dc
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_task_mixed_children.md
@@ -0,0 +1,4 @@
+- [ ] parent task
+    - [ ] child task 1
+    - child list item 1
+    - [ ] child task 2
diff --git a/tests/Obsidian/Cache.test.ts b/tests/Obsidian/Cache.test.ts
index 60754c59..57f84d6d 100644
--- a/tests/Obsidian/Cache.test.ts
+++ b/tests/Obsidian/Cache.test.ts
@@ -3,9 +3,9 @@
  */
 import moment from 'moment/moment';
 import type { CachedMetadata } from 'obsidian';
-import type { Task } from 'Task/Task';
 import { logging } from '../../src/lib/logging';
 import { getTasksFromFileContent2 } from '../../src/Obsidian/Cache';
+import type { ListItem } from '../../src/Task/ListItem';
 import { inheritance_1parent1child } from './__test_data__/inheritance_1parent1child';
 import { inheritance_1parent1child1newroot_after_header } from './__test_data__/inheritance_1parent1child1newroot_after_header';
 import { inheritance_1parent1child1sibling_emptystring } from './__test_data__/inheritance_1parent1child1sibling_emptystring';
@@ -16,6 +16,13 @@ import { inheritance_1parent2children2grandchildren } from './__test_data__/inhe
 import { inheritance_1parent2children2grandchildren1sibling } from './__test_data__/inheritance_1parent2children2grandchildren1sibling';
 import { inheritance_1parent2children2grandchildren1sibling_start_with_heading } from './__test_data__/inheritance_1parent2children2grandchildren1sibling_start_with_heading';
 import { inheritance_2siblings } from './__test_data__/inheritance_2siblings';
+import { inheritance_listitem_task } from './__test_data__/inheritance_listitem_task';
+import { inheritance_listitem_task_siblings } from './__test_data__/inheritance_listitem_task_siblings';
+import { inheritance_task_2listitem_3task } from './__test_data__/inheritance_task_2listitem_3task';
+import { inheritance_task_listitem } from './__test_data__/inheritance_task_listitem';
+import { inheritance_task_listitem_mixed_grandchildren } from './__test_data__/inheritance_task_listitem_mixed_grandchildren';
+import { inheritance_task_listitem_task } from './__test_data__/inheritance_task_listitem_task';
+import { inheritance_task_mixed_children } from './__test_data__/inheritance_task_mixed_children';
 import { one_task } from './__test_data__/one_task';
 
 window.moment = moment;
@@ -69,13 +76,13 @@ function readTasksFromSimulatedFile(testData: SimulatedFile) {
     );
 }
 
-function testRootAndChildren(root: Task, children: Task[]) {
+function testRootAndChildren(root: ListItem, children: ListItem[]) {
     expect(root.parent).toEqual(null);
 
     testChildren(root, children);
 }
 
-function testChildren(parent: Task, childList: Task[]) {
+function testChildren(parent: ListItem, childList: ListItem[]) {
     expect(parent.children).toEqual(childList);
     for (const child of childList) {
         expect(child.parent?.originalMarkdown).toEqual(parent.originalMarkdown);
@@ -83,6 +90,48 @@ function testChildren(parent: Task, childList: Task[]) {
     }
 }
 
+/**
+ * Print a snapshot of a {@link ListItem} hierarchy based on parent-child relationships. To achieve this,
+ * any indentation in {@link ListItem.originalMarkdown} will be trimmed and new indentation based
+ * on parent-child relationships will be added to the snapshot.
+ *
+ * The type of the {@link ListItem} will be printed as well to avoid type mismatch with {@link Task}
+ * since {@link Task} extends {@link ListItem}.
+ *
+ * @param listItem
+ * @param depth of the starting tree. Set to 0 for root {@link ListItem}.
+ */
+function printHierarchy(listItem: ListItem, depth: number): string {
+    const indentation = ' '.repeat(depth * 4);
+    const trimmedMarkdown = listItem.originalMarkdown.trim();
+    const type = listItem.constructor.name;
+
+    const listItemLine = `${indentation}${trimmedMarkdown} : ${type}`;
+    const childrenLines = listItem.children.map((child) => printHierarchy(child, depth + 1)).join('');
+
+    return [listItemLine, childrenLines].join('\n');
+}
+
+/**
+ * Print hierarchies from root {@link ListItem}s found in a {@link ListItem} array. A {@link ListItem} is considered
+ * a root if it has no parent:
+ * @example
+ * ListItem.parent === null
+ *
+ * @param listItems
+ */
+function printRoots(listItems: ListItem[]) {
+    let rootHierarchies = '';
+
+    for (const listItem of listItems) {
+        if (listItem.parent === null) {
+            rootHierarchies += printHierarchy(listItem, 0);
+        }
+    }
+
+    return rootHierarchies;
+}
+
 describe('cache', () => {
     it('should read one task', () => {
         const tasks = readTasksFromSimulatedFile(one_task);
@@ -285,4 +334,140 @@ describe('cache', () => {
 
         testRootAndChildren(sibling, []);
     });
+
+    it('should read task and listItem siblings', () => {
+        const tasks = readTasksFromSimulatedFile(inheritance_listitem_task_siblings);
+        expect(inheritance_listitem_task_siblings.fileContents).toMatchInlineSnapshot(`
+            "- list item
+            - [ ] task
+            "
+        `);
+
+        expect(tasks.length).toEqual(1);
+
+        const [task] = tasks;
+
+        testRootAndChildren(task, []);
+    });
+
+    it('should read child task and parent listItem', () => {
+        const tasks = readTasksFromSimulatedFile(inheritance_listitem_task);
+        expect(inheritance_listitem_task.fileContents).toMatchInlineSnapshot(`
+            "- parent list item
+                - [ ] child task
+            "
+        `);
+
+        expect(tasks.length).toEqual(1);
+
+        expect(printRoots(tasks)).toMatchInlineSnapshot(`
+            "- [ ] child task : Task
+            "
+        `);
+    });
+
+    it('should read parent task and child listItem', () => {
+        const tasks = readTasksFromSimulatedFile(inheritance_task_listitem);
+        expect(inheritance_task_listitem.fileContents).toMatchInlineSnapshot(`
+            "- [ ] parent task
+                - child list item
+            "
+        `);
+
+        expect(tasks.length).toEqual(1);
+
+        expect(printRoots(tasks)).toMatchInlineSnapshot(`
+            "- [ ] parent task : Task
+                - child list item : ListItem
+            "
+        `);
+    });
+
+    it('should read parent task, child listItem and grandchild task', () => {
+        const tasks = readTasksFromSimulatedFile(inheritance_task_listitem_task);
+        expect(inheritance_task_listitem_task.fileContents).toMatchInlineSnapshot(`
+            "- [ ] parent task
+                - child list item
+                    - [ ] grandchild task
+            "
+        `);
+
+        expect(tasks.length).toEqual(2);
+
+        expect(printRoots(tasks)).toMatchInlineSnapshot(`
+            "- [ ] parent task : Task
+                - child list item : ListItem
+                    - [ ] grandchild task : Task
+            "
+        `);
+    });
+
+    it('should read parent task, two child listItems and 3 grandchild tasks', () => {
+        const tasks = readTasksFromSimulatedFile(inheritance_task_2listitem_3task);
+        expect(inheritance_task_2listitem_3task.fileContents).toMatchInlineSnapshot(`
+            "- [ ] parent task
+                - child list item 1
+                    - [ ] grandchild task 1
+                    - [ ] grandchild task 2
+                - child list item 2
+                    - [ ] grandchild task 3
+            "
+        `);
+
+        expect(tasks.length).toEqual(4);
+
+        expect(printRoots(tasks)).toMatchInlineSnapshot(`
+            "- [ ] parent task : Task
+                - child list item 1 : ListItem
+                    - [ ] grandchild task 1 : Task
+                    - [ ] grandchild task 2 : Task
+                - child list item 2 : ListItem
+                    - [ ] grandchild task 3 : Task
+            "
+        `);
+    });
+
+    it('should read parent task with mixed children', () => {
+        const tasks = readTasksFromSimulatedFile(inheritance_task_mixed_children);
+        expect(inheritance_task_mixed_children.fileContents).toMatchInlineSnapshot(`
+            "- [ ] parent task
+                - [ ] child task 1
+                - child list item 1
+                - [ ] child task 2
+            "
+        `);
+
+        expect(tasks.length).toEqual(3);
+
+        expect(printRoots(tasks)).toMatchInlineSnapshot(`
+            "- [ ] parent task : Task
+                - [ ] child task 1 : Task
+                - child list item 1 : ListItem
+                - [ ] child task 2 : Task
+            "
+        `);
+    });
+
+    it('should read parent task and child list item with mixed children', () => {
+        const tasks = readTasksFromSimulatedFile(inheritance_task_listitem_mixed_grandchildren);
+        expect(inheritance_task_listitem_mixed_grandchildren.fileContents).toMatchInlineSnapshot(`
+            "- [ ] parent task
+                - child list item
+                    - grandchild list item 1
+                    - [ ] grandchild task
+                    - grandchild list item 2
+            "
+        `);
+
+        expect(tasks.length).toEqual(2);
+
+        expect(printRoots(tasks)).toMatchInlineSnapshot(`
+            "- [ ] parent task : Task
+                - child list item : ListItem
+                    - grandchild list item 1 : ListItem
+                    - [ ] grandchild task : Task
+                    - grandchild list item 2 : ListItem
+            "
+        `);
+    });
 });
diff --git a/tests/Obsidian/__test_data__/inheritance_listitem_task.ts b/tests/Obsidian/__test_data__/inheritance_listitem_task.ts
new file mode 100644
index 00000000..b3bea0d7
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_listitem_task.ts
@@ -0,0 +1,56 @@
+export const inheritance_listitem_task = {
+    filePath: 'Test Data/inheritance_listitem_task.md',
+    fileContents: '- parent list item\n    - [ ] child task\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 1,
+                        col: 20,
+                        offset: 39,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 18,
+                        offset: 18,
+                    },
+                },
+                parent: -1,
+            },
+            {
+                position: {
+                    start: {
+                        line: 1,
+                        col: 2,
+                        offset: 21,
+                    },
+                    end: {
+                        line: 1,
+                        col: 20,
+                        offset: 39,
+                    },
+                },
+                parent: 0,
+                task: ' ',
+            },
+        ],
+    },
+};
diff --git a/tests/Obsidian/__test_data__/inheritance_listitem_task_siblings.ts b/tests/Obsidian/__test_data__/inheritance_listitem_task_siblings.ts
new file mode 100644
index 00000000..fd18849d
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_listitem_task_siblings.ts
@@ -0,0 +1,56 @@
+export const inheritance_listitem_task_siblings = {
+    filePath: 'Test Data/inheritance_listitem_task_siblings.md',
+    fileContents: '- list item\n- [ ] task\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 1,
+                        col: 10,
+                        offset: 22,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 11,
+                        offset: 11,
+                    },
+                },
+                parent: -1,
+            },
+            {
+                position: {
+                    start: {
+                        line: 1,
+                        col: 0,
+                        offset: 12,
+                    },
+                    end: {
+                        line: 1,
+                        col: 10,
+                        offset: 22,
+                    },
+                },
+                parent: -1,
+                task: ' ',
+            },
+        ],
+    },
+};
diff --git a/tests/Obsidian/__test_data__/inheritance_task_2listitem_3task.ts b/tests/Obsidian/__test_data__/inheritance_task_2listitem_3task.ts
new file mode 100644
index 00000000..2b7eef4e
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_task_2listitem_3task.ts
@@ -0,0 +1,125 @@
+export const inheritance_task_2listitem_3task = {
+    filePath: 'Test Data/inheritance_task_2listitem_3task.md',
+    fileContents:
+        '- [ ] parent task\n' +
+        '    - child list item 1\n' +
+        '        - [ ] grandchild task 1\n' +
+        '        - [ ] grandchild task 2\n' +
+        '    - child list item 2\n' +
+        '        - [ ] grandchild task 3\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 5,
+                        col: 31,
+                        offset: 161,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 17,
+                        offset: 17,
+                    },
+                },
+                parent: -1,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 1,
+                        col: 2,
+                        offset: 20,
+                    },
+                    end: {
+                        line: 1,
+                        col: 23,
+                        offset: 41,
+                    },
+                },
+                parent: 0,
+            },
+            {
+                position: {
+                    start: {
+                        line: 2,
+                        col: 6,
+                        offset: 48,
+                    },
+                    end: {
+                        line: 2,
+                        col: 31,
+                        offset: 73,
+                    },
+                },
+                parent: 1,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 3,
+                        col: 6,
+                        offset: 80,
+                    },
+                    end: {
+                        line: 3,
+                        col: 31,
+                        offset: 105,
+                    },
+                },
+                parent: 1,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 4,
+                        col: 2,
+                        offset: 108,
+                    },
+                    end: {
+                        line: 4,
+                        col: 23,
+                        offset: 129,
+                    },
+                },
+                parent: 0,
+            },
+            {
+                position: {
+                    start: {
+                        line: 5,
+                        col: 6,
+                        offset: 136,
+                    },
+                    end: {
+                        line: 5,
+                        col: 31,
+                        offset: 161,
+                    },
+                },
+                parent: 4,
+                task: ' ',
+            },
+        ],
+    },
+};
diff --git a/tests/Obsidian/__test_data__/inheritance_task_listitem.ts b/tests/Obsidian/__test_data__/inheritance_task_listitem.ts
new file mode 100644
index 00000000..d906c8d6
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_task_listitem.ts
@@ -0,0 +1,56 @@
+export const inheritance_task_listitem = {
+    filePath: 'Test Data/inheritance_task_listitem.md',
+    fileContents: '- [ ] parent task\n    - child list item\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 1,
+                        col: 21,
+                        offset: 39,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                parent: -1,
+                task: ' ',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 17,
+                        offset: 17,
+                    },
+                },
+            },
+            {
+                parent: 0,
+                position: {
+                    start: {
+                        line: 1,
+                        col: 2,
+                        offset: 20,
+                    },
+                    end: {
+                        line: 1,
+                        col: 21,
+                        offset: 39,
+                    },
+                },
+            },
+        ],
+    },
+};
diff --git a/tests/Obsidian/__test_data__/inheritance_task_listitem_mixed_grandchildren.ts b/tests/Obsidian/__test_data__/inheritance_task_listitem_mixed_grandchildren.ts
new file mode 100644
index 00000000..a79709d0
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_task_listitem_mixed_grandchildren.ts
@@ -0,0 +1,107 @@
+export const inheritance_task_listitem_mixed_grandchildren = {
+    filePath: 'Test Data/inheritance_task_listitem_mixed_grandchildren.md',
+    fileContents:
+        '- [ ] parent task\n' +
+        '    - child list item\n' +
+        '        - grandchild list item 1\n' +
+        '        - [ ] grandchild task\n' +
+        '        - grandchild list item 2\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 4,
+                        col: 32,
+                        offset: 135,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                parent: -1,
+                task: ' ',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 17,
+                        offset: 17,
+                    },
+                },
+            },
+            {
+                parent: 0,
+                position: {
+                    start: {
+                        line: 1,
+                        col: 2,
+                        offset: 20,
+                    },
+                    end: {
+                        line: 1,
+                        col: 21,
+                        offset: 39,
+                    },
+                },
+            },
+            {
+                parent: 1,
+                position: {
+                    start: {
+                        line: 2,
+                        col: 6,
+                        offset: 46,
+                    },
+                    end: {
+                        line: 2,
+                        col: 32,
+                        offset: 72,
+                    },
+                },
+            },
+            {
+                parent: 1,
+                task: ' ',
+                position: {
+                    start: {
+                        line: 3,
+                        col: 6,
+                        offset: 79,
+                    },
+                    end: {
+                        line: 3,
+                        col: 29,
+                        offset: 102,
+                    },
+                },
+            },
+            {
+                parent: 1,
+                position: {
+                    start: {
+                        line: 4,
+                        col: 6,
+                        offset: 109,
+                    },
+                    end: {
+                        line: 4,
+                        col: 32,
+                        offset: 135,
+                    },
+                },
+            },
+        ],
+    },
+};
diff --git a/tests/Obsidian/__test_data__/inheritance_task_listitem_task.ts b/tests/Obsidian/__test_data__/inheritance_task_listitem_task.ts
new file mode 100644
index 00000000..11a8609a
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_task_listitem_task.ts
@@ -0,0 +1,72 @@
+export const inheritance_task_listitem_task = {
+    filePath: 'Test Data/inheritance_task_listitem_task.md',
+    fileContents: '- [ ] parent task\n    - child list item\n        - [ ] grandchild task\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 2,
+                        col: 29,
+                        offset: 69,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                parent: -1,
+                task: ' ',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 17,
+                        offset: 17,
+                    },
+                },
+            },
+            {
+                parent: 0,
+                position: {
+                    start: {
+                        line: 1,
+                        col: 2,
+                        offset: 20,
+                    },
+                    end: {
+                        line: 1,
+                        col: 21,
+                        offset: 39,
+                    },
+                },
+            },
+            {
+                parent: 1,
+                task: ' ',
+                position: {
+                    start: {
+                        line: 2,
+                        col: 6,
+                        offset: 46,
+                    },
+                    end: {
+                        line: 2,
+                        col: 29,
+                        offset: 69,
+                    },
+                },
+            },
+        ],
+    },
+};
diff --git a/tests/Obsidian/__test_data__/inheritance_task_mixed_children.ts b/tests/Obsidian/__test_data__/inheritance_task_mixed_children.ts
new file mode 100644
index 00000000..c8f5861b
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_task_mixed_children.ts
@@ -0,0 +1,89 @@
+export const inheritance_task_mixed_children = {
+    filePath: 'Test Data/inheritance_task_mixed_children.md',
+    fileContents:
+        '- [ ] parent task\n' + '    - [ ] child task 1\n' + '    - child list item 1\n' + '    - [ ] child task 2\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 3,
+                        col: 22,
+                        offset: 87,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 17,
+                        offset: 17,
+                    },
+                },
+                parent: -1,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 1,
+                        col: 2,
+                        offset: 20,
+                    },
+                    end: {
+                        line: 1,
+                        col: 22,
+                        offset: 40,
+                    },
+                },
+                parent: 0,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 2,
+                        col: 2,
+                        offset: 43,
+                    },
+                    end: {
+                        line: 2,
+                        col: 23,
+                        offset: 64,
+                    },
+                },
+                parent: 0,
+            },
+            {
+                position: {
+                    start: {
+                        line: 3,
+                        col: 2,
+                        offset: 67,
+                    },
+                    end: {
+                        line: 3,
+                        col: 22,
+                        offset: 87,
+                    },
+                },
+                parent: 0,
+                task: ' ',
+            },
+        ],
+    },
+};