Skip to content

Commit cf8bdf9

Browse files
authored
Merge pull request #3291 from ilandikov/fix-render-non-task-list-items
fix: render non-task list items in tree
2 parents 540acf0 + 6d8a8bf commit cf8bdf9

12 files changed

+318
-2
lines changed

docs/Queries/Layout.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ The `show tree` instruction enables us to see the parent/child relationships in
136136
137137
- For now, **all child tasks and list items are displayed**, regardless of whether they match the query.
138138
- In the screenshot above, `Decide who to invite` did not match the `not done` query, but it is still shown.
139+
- If you are using the Global Filter, any child tasks without the Global Filter are rendered without their checkbox. We are tracking this in [issue #3170](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/3170).
139140
- Any **sorting instructions only affect the sorting of the left-most tasks** in the results list.
140141
- Child tasks and list items are displayed in the order that they appear in the file. They are not affected by any `sort by` instructions.
141142
- For now, the **tree layout is turned off by default**, whilst we explore how it should interact with the filtering instructions.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
- [ ] #task task parent
2+
- [ ] #task task child
3+
- [ ] non-task child
4+
- list item child
5+
6+
```tasks
7+
filename includes {{query.file.filename}}
8+
show tree
9+
```

src/Obsidian/FileParser.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,7 @@ export class FileParser {
119119
sectionIndex: number,
120120
) {
121121
if (listItem.task === undefined) {
122-
const parentListItem: ListItem | null = this.line2ListItem.get(listItem.parent) ?? null;
123-
this.line2ListItem.set(lineNumber, new ListItem(line, parentListItem));
122+
this.createListItem(listItem, line, lineNumber);
124123
return sectionIndex;
125124
}
126125
let task;
@@ -148,10 +147,18 @@ export class FileParser {
148147
sectionIndex++;
149148
this.tasks.push(task);
150149
}
150+
} else {
151+
// Treat tasks without the global filter as list items
152+
this.createListItem(listItem, line, lineNumber);
151153
}
152154
} catch (e) {
153155
this.errorReporter(e, this.filePath, listItem, line);
154156
}
155157
return sectionIndex;
156158
}
159+
160+
private createListItem(listItem: ListItemCache, line: string, lineNumber: number) {
161+
const parentListItem: ListItem | null = this.line2ListItem.get(listItem.parent) ?? null;
162+
this.line2ListItem.set(lineNumber, new ListItem(line, parentListItem));
163+
}
157164
}

src/Task/ListItem.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ export class ListItem {
88
public readonly parent: ListItem | null = null;
99
public readonly children: ListItem[] = [];
1010
public readonly description: string;
11+
public readonly statusCharacter: string | null = null;
1112

1213
constructor(originalMarkdown: string, parent: ListItem | null) {
1314
this.description = originalMarkdown.replace(TaskRegularExpressions.listItemRegex, '').trim();
15+
const nonTaskMatch = RegExp(TaskRegularExpressions.nonTaskRegex).exec(originalMarkdown);
16+
if (nonTaskMatch) {
17+
this.description = nonTaskMatch[5].trim();
18+
this.statusCharacter = nonTaskMatch[4] ?? null;
19+
}
1420
this.originalMarkdown = originalMarkdown;
1521
this.parent = parent;
1622

@@ -87,6 +93,8 @@ export class ListItem {
8793
return false;
8894
}
8995

96+
// Not testing status character as it is implied from the original markdown
97+
9098
return ListItem.listsAreIdentical(this.children, other.children);
9199
}
92100

src/Task/TaskRegularExpressions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export class TaskRegularExpressions {
3131
);
3232

3333
// Used with the "Create or Edit Task" command to parse indentation and status if present
34+
// It matches the following:
35+
// - Indentation
36+
// - List marker
37+
// - Checkbox with status character
38+
// - Status character
39+
// - Rest of task after checkbox markdown
3440
public static readonly nonTaskRegex = new RegExp(
3541
TaskRegularExpressions.indentationRegex.source +
3642
TaskRegularExpressions.listMarkerRegex.source +

tests/Obsidian/AllCacheSampleData.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import inheritance_2siblings from './__test_data__/inheritance_2siblings.json';
2626
import inheritance_listitem_listitem_task from './__test_data__/inheritance_listitem_listitem_task.json';
2727
import inheritance_listitem_task from './__test_data__/inheritance_listitem_task.json';
2828
import inheritance_listitem_task_siblings from './__test_data__/inheritance_listitem_task_siblings.json';
29+
import inheritance_non_task_child from './__test_data__/inheritance_non_task_child.json';
2930
import inheritance_rendering_sample from './__test_data__/inheritance_rendering_sample.json';
3031
import inheritance_task_2listitem_3task from './__test_data__/inheritance_task_2listitem_3task.json';
3132
import inheritance_task_listitem from './__test_data__/inheritance_task_listitem.json';
@@ -96,6 +97,7 @@ export function allCacheSampleData() {
9697
inheritance_listitem_listitem_task,
9798
inheritance_listitem_task,
9899
inheritance_listitem_task_siblings,
100+
inheritance_non_task_child,
99101
inheritance_rendering_sample,
100102
inheritance_task_2listitem_3task,
101103
inheritance_task_listitem,

tests/Obsidian/Cache.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import moment from 'moment/moment';
55
import type { CachedMetadata } from 'obsidian';
6+
import { GlobalFilter } from '../../src/Config/GlobalFilter';
67
import type { ListItem } from '../../src/Task/ListItem';
78
import { getTasksFileFromMockData, listPathAndData } from '../TestingTools/MockDataHelpers';
89
import inheritance_1parent1child from './__test_data__/inheritance_1parent1child.json';
@@ -19,6 +20,7 @@ import inheritance_2siblings from './__test_data__/inheritance_2siblings.json';
1920
import inheritance_listitem_listitem_task from './__test_data__/inheritance_listitem_listitem_task.json';
2021
import inheritance_listitem_task from './__test_data__/inheritance_listitem_task.json';
2122
import inheritance_listitem_task_siblings from './__test_data__/inheritance_listitem_task_siblings.json';
23+
import inheritance_non_task_child from './__test_data__/inheritance_non_task_child.json';
2224
import inheritance_task_2listitem_3task from './__test_data__/inheritance_task_2listitem_3task.json';
2325
import inheritance_task_listitem from './__test_data__/inheritance_task_listitem.json';
2426
import inheritance_task_listitem_mixed_grandchildren from './__test_data__/inheritance_task_listitem_mixed_grandchildren.json';
@@ -95,6 +97,10 @@ function printRoots(listItems: ListItem[]) {
9597
return rootHierarchies;
9698
}
9799

100+
afterEach(() => {
101+
GlobalFilter.getInstance().reset();
102+
});
103+
98104
describe('cache', () => {
99105
it('should read one task', () => {
100106
const tasks = readTasksFromSimulatedFile(one_task);
@@ -480,6 +486,35 @@ describe('cache', () => {
480486
`);
481487
});
482488

489+
it('should read non task check box when global filter is enabled', () => {
490+
GlobalFilter.getInstance().set('#task');
491+
492+
const data = inheritance_non_task_child;
493+
const tasks = readTasksFromSimulatedFile(data);
494+
expect(data.fileContents).toMatchInlineSnapshot(`
495+
"- [ ] #task task parent
496+
- [ ] #task task child
497+
- [ ] non-task child
498+
- list item child
499+
500+
\`\`\`tasks
501+
filename includes {{query.file.filename}}
502+
show tree
503+
\`\`\`
504+
"
505+
`);
506+
507+
expect(tasks.length).toEqual(2);
508+
509+
expect(printRoots(tasks)).toMatchInlineSnapshot(`
510+
"- [ ] #task task parent : Task
511+
- [ ] #task task child : Task
512+
- [ ] non-task child : ListItem
513+
- list item child : ListItem
514+
"
515+
`);
516+
});
517+
483518
it('callout', () => {
484519
const tasks = readTasksFromSimulatedFile(callout);
485520
expect(callout.fileContents).toMatchInlineSnapshot(`
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{
2+
"cachedMetadata": {
3+
"listItems": [
4+
{
5+
"parent": -1,
6+
"position": {
7+
"end": {
8+
"col": 24,
9+
"line": 0,
10+
"offset": 24
11+
},
12+
"start": {
13+
"col": 0,
14+
"line": 0,
15+
"offset": 0
16+
}
17+
},
18+
"task": " "
19+
},
20+
{
21+
"parent": 0,
22+
"position": {
23+
"end": {
24+
"col": 26,
25+
"line": 1,
26+
"offset": 51
27+
},
28+
"start": {
29+
"col": 3,
30+
"line": 1,
31+
"offset": 28
32+
}
33+
},
34+
"task": " "
35+
},
36+
{
37+
"parent": 0,
38+
"position": {
39+
"end": {
40+
"col": 24,
41+
"line": 2,
42+
"offset": 76
43+
},
44+
"start": {
45+
"col": 3,
46+
"line": 2,
47+
"offset": 55
48+
}
49+
},
50+
"task": " "
51+
},
52+
{
53+
"parent": 0,
54+
"position": {
55+
"end": {
56+
"col": 21,
57+
"line": 3,
58+
"offset": 98
59+
},
60+
"start": {
61+
"col": 3,
62+
"line": 3,
63+
"offset": 80
64+
}
65+
}
66+
}
67+
],
68+
"sections": [
69+
{
70+
"position": {
71+
"end": {
72+
"col": 21,
73+
"line": 3,
74+
"offset": 98
75+
},
76+
"start": {
77+
"col": 0,
78+
"line": 0,
79+
"offset": 0
80+
}
81+
},
82+
"type": "list"
83+
},
84+
{
85+
"position": {
86+
"end": {
87+
"col": 3,
88+
"line": 8,
89+
"offset": 164
90+
},
91+
"start": {
92+
"col": 0,
93+
"line": 5,
94+
"offset": 100
95+
}
96+
},
97+
"type": "code"
98+
}
99+
],
100+
"tags": [
101+
{
102+
"position": {
103+
"end": {
104+
"col": 12,
105+
"line": 0,
106+
"offset": 12
107+
},
108+
"start": {
109+
"col": 7,
110+
"line": 0,
111+
"offset": 7
112+
}
113+
},
114+
"tag": "#task"
115+
},
116+
{
117+
"position": {
118+
"end": {
119+
"col": 15,
120+
"line": 1,
121+
"offset": 40
122+
},
123+
"start": {
124+
"col": 10,
125+
"line": 1,
126+
"offset": 35
127+
}
128+
},
129+
"tag": "#task"
130+
}
131+
]
132+
},
133+
"fileContents": "- [ ] #task task parent\n - [ ] #task task child\n - [ ] non-task child\n - list item child\n\n```tasks\nfilename includes {{query.file.filename}}\nshow tree\n```\n",
134+
"filePath": "Test Data/inheritance_non_task_child.md",
135+
"getAllTags": [
136+
"#task",
137+
"#task"
138+
],
139+
"obsidianApiVersion": "1.8.0",
140+
"parseFrontMatterTags": null
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!--
2+
- [ ] #task task parent
3+
- [ ] #task task child
4+
-->
5+
6+
<div>
7+
<ul class="contains-task-list plugin-tasks-query-result tasks-layout-hide-urgency">
8+
<li
9+
class="task-list-item plugin-tasks-list-item"
10+
data-task-priority="normal"
11+
data-task=""
12+
data-line="0"
13+
data-task-status-name="Todo"
14+
data-task-status-type="TODO">
15+
<input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="0" />
16+
<span class="tasks-list-text">
17+
<span class="task-description"><span>#task task parent</span></span>
18+
</span>
19+
<span class="task-extras">
20+
<span class="tasks-backlink">
21+
(
22+
<a rel="noopener" target="_blank" class="internal-link">inheritance_non_task_child</a>
23+
)
24+
</span>
25+
<a class="tasks-edit" title="Edit task" href="#"></a>
26+
</span>
27+
<ul class="contains-task-list plugin-tasks-query-result tasks-layout-hide-urgency">
28+
<li
29+
class="task-list-item plugin-tasks-list-item"
30+
data-task-priority="normal"
31+
data-task=""
32+
data-line="0"
33+
data-task-status-name="Todo"
34+
data-task-status-type="TODO">
35+
<input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="0" />
36+
<span class="tasks-list-text">
37+
<span class="task-description"><span>#task task child</span></span>
38+
</span>
39+
<span class="task-extras">
40+
<span class="tasks-backlink">
41+
(
42+
<a rel="noopener" target="_blank" class="internal-link">inheritance_non_task_child</a>
43+
)
44+
</span>
45+
<a class="tasks-edit" title="Edit task" href="#"></a>
46+
</span>
47+
</li>
48+
<li><span>non-task child</span></li>
49+
<li><span>list item child</span></li>
50+
</ul>
51+
</li>
52+
</ul>
53+
<div class="task-count">2 tasks</div>
54+
</div>

tests/Renderer/QueryResultsRenderer.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
*/
44
import moment from 'moment';
55
import type { Task } from 'Task/Task';
6+
import { GlobalFilter } from '../../src/Config/GlobalFilter';
67
import { State } from '../../src/Obsidian/Cache';
78
import { QueryResultsRenderer } from '../../src/Renderer/QueryResultsRenderer';
89
import { TasksFile } from '../../src/Scripting/TasksFile';
10+
import inheritance_non_task_child from '../Obsidian/__test_data__/inheritance_non_task_child.json';
911
import inheritance_rendering_sample from '../Obsidian/__test_data__/inheritance_rendering_sample.json';
1012
import inheritance_task_2listitem_3task from '../Obsidian/__test_data__/inheritance_task_2listitem_3task.json';
1113
import { readTasksFromSimulatedFile } from '../Obsidian/SimulatedFile';
@@ -24,6 +26,7 @@ beforeEach(() => {
2426

2527
afterEach(() => {
2628
jest.useRealTimers();
29+
GlobalFilter.getInstance().reset();
2730
});
2831

2932
function makeQueryResultsRenderer(source: string, tasksFile: TasksFile) {
@@ -92,6 +95,12 @@ ${toMarkdown(allTasks)}
9295
const allTasks = readTasksFromSimulatedFile(inheritance_task_2listitem_3task);
9396
await verifyRenderedTasksHTML(allTasks, showTree + 'description includes grandchild');
9497
});
98+
99+
it('should render non task check box when global filter is enabled', async () => {
100+
GlobalFilter.getInstance().set('#task');
101+
const allTasks = readTasksFromSimulatedFile(inheritance_non_task_child);
102+
await verifyRenderedTasksHTML(allTasks, showTree);
103+
});
95104
});
96105

97106
describe('QueryResultsRenderer - responding to file edits', () => {

0 commit comments

Comments
 (0)