Skip to content

Commit 3700d09

Browse files
authored
Merge pull request #3221 from obsidian-tasks-group/explain-grouping-and-sorting
feat: 'explain' shows effect of Line Continuations & Placeholders
2 parents 7ff004e + 3ce58c8 commit 3700d09

File tree

7 files changed

+191
-19
lines changed

7 files changed

+191
-19
lines changed

src/Query/Explain/Explainer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,15 @@ export class Explainer {
6161
return this.indent('No grouping instructions supplied.\n');
6262
}
6363

64-
return query.grouping.map((group) => this.indentation + group.instruction).join('\n') + '\n';
64+
return query.grouping.map((group) => group.statement.explainStatement(this.indentation)).join('\n\n') + '\n';
6565
}
6666

6767
public explainSorters(query: Query) {
6868
if (query.sorting.length === 0) {
6969
return this.indent('No sorting instructions supplied.\n');
7070
}
7171

72-
return query.sorting.map((group) => this.indentation + group.instruction).join('\n') + '\n';
72+
return query.sorting.map((sort) => sort.statement.explainStatement(this.indentation)).join('\n\n') + '\n';
7373
}
7474

7575
public explainQueryLimits(query: Query) {

src/Query/Group/Grouper.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Task } from '../../Task/Task';
22
import type { SearchInfo } from '../SearchInfo';
3+
import { Statement } from '../Statement';
34

45
/**
56
* A group-naming function, that takes a Task object and returns zero or more
@@ -25,7 +26,8 @@ export type GrouperFunction = (task: Task, searchInfo: SearchInfo) => string[];
2526
* @see {@link TaskGroups} for how to use {@link Grouper} objects to group tasks together.
2627
*/
2728
export class Grouper {
28-
public instruction: string;
29+
/** _statement may be updated later with {@link setStatement} */
30+
private _statement: Statement;
2931

3032
/**
3133
* The type of grouper, for example 'tags' or 'due'.
@@ -46,9 +48,27 @@ export class Grouper {
4648
public readonly reverse: boolean;
4749

4850
constructor(instruction: string, property: string, grouper: GrouperFunction, reverse: boolean) {
49-
this.instruction = instruction;
51+
this._statement = new Statement(instruction, instruction);
5052
this.property = property;
5153
this.grouper = grouper;
5254
this.reverse = reverse;
5355
}
56+
57+
/**
58+
* Optionally record more detail about the source statement.
59+
*
60+
* In tests, we only care about the actual instruction being parsed and executed.
61+
* However, in {@link Query}, we want the ability to show user more information.
62+
*/
63+
public setStatement(statement: Statement) {
64+
this._statement = statement;
65+
}
66+
67+
public get statement(): Statement {
68+
return this._statement;
69+
}
70+
71+
public get instruction(): string {
72+
return this._statement.anyPlaceholdersExpanded;
73+
}
5474
}

src/Query/Query.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ export class Query implements IQuery {
105105
case this.limitRegexp.test(line):
106106
this.parseLimit(line);
107107
break;
108-
case this.parseSortBy(line):
108+
case this.parseSortBy(line, statement):
109109
break;
110-
case this.parseGroupBy(line):
110+
case this.parseGroupBy(line, statement):
111111
break;
112112
case this.hideOptionsRegexp.test(line):
113113
this.parseHideOptions(line);
@@ -352,9 +352,10 @@ ${statement.explainStatement(' ')}
352352
}
353353
}
354354

355-
private parseSortBy(line: string): boolean {
355+
private parseSortBy(line: string, statement: Statement): boolean {
356356
const sortingMaybe = FilterParser.parseSorter(line);
357357
if (sortingMaybe) {
358+
sortingMaybe.setStatement(statement);
358359
this._sorting.push(sortingMaybe);
359360
return true;
360361
}
@@ -366,11 +367,13 @@ ${statement.explainStatement(' ')}
366367
* classes.
367368
*
368369
* @param line
370+
* @param statement
369371
* @private
370372
*/
371-
private parseGroupBy(line: string): boolean {
373+
private parseGroupBy(line: string, statement: Statement): boolean {
372374
const groupingMaybe = FilterParser.parseGrouper(line);
373375
if (groupingMaybe) {
376+
groupingMaybe.setStatement(statement);
374377
this._grouping.push(groupingMaybe);
375378
return true;
376379
}

src/Query/Sort/Sorter.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Task } from '../../Task/Task';
22
import type { SearchInfo } from '../SearchInfo';
3+
import { Statement } from '../Statement';
34

45
/**
56
* A sorting function, that takes two Task objects and returns
@@ -20,7 +21,8 @@ export type Comparator = (a: Task, b: Task, searchInfo: SearchInfo) => number;
2021
* It stores the comparison function as a {@link Comparator}.
2122
*/
2223
export class Sorter {
23-
public readonly instruction: string;
24+
/** _statement may be updated later with {@link setStatement} */
25+
private _statement: Statement;
2426
public readonly property: string;
2527
public readonly comparator: Comparator;
2628

@@ -34,11 +36,29 @@ export class Sorter {
3436
* @param reverse - whether the sort order should be reversed.
3537
*/
3638
constructor(instruction: string, property: string, comparator: Comparator, reverse: boolean) {
37-
this.instruction = instruction;
39+
this._statement = new Statement(instruction, instruction);
3840
this.property = property;
3941
this.comparator = Sorter.maybeReverse(reverse, comparator);
4042
}
4143

44+
/**
45+
* Optionally record more detail about the source statement.
46+
*
47+
* In tests, we only care about the actual instruction being parsed and executed.
48+
* However, in {@link Query}, we want the ability to show user more information.
49+
*/
50+
public setStatement(statement: Statement) {
51+
this._statement = statement;
52+
}
53+
54+
public get statement(): Statement {
55+
return this._statement;
56+
}
57+
58+
public get instruction(): string {
59+
return this._statement.anyPlaceholdersExpanded;
60+
}
61+
4262
private static maybeReverse(reverse: boolean, comparator: Comparator) {
4363
return reverse ? Sorter.makeReversedComparator(comparator) : comparator;
4464
}

tests/Query/Explain/Explainer.test.ts

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ describe('explain errors', () => {
4646

4747
describe('explain everything', () => {
4848
const sampleOfAllInstructionTypes = `
49+
filter by function \\
50+
task.path === '{{query.file.path}}'
4951
not done
5052
(has start date) AND (description includes some)
5153
54+
group by function \\
55+
task.path.includes('{{query.file.path}}')
5256
group by priority reverse
5357
group by happens
5458
59+
sort by function \\
60+
task.path.includes('{{query.file.path}}')
5561
sort by description reverse
5662
sort by path
5763
@@ -65,19 +71,39 @@ limit groups 3
6571
// Disable sort instructions
6672
updateSettings({ debugSettings: new DebugSettings(true) });
6773

68-
const query = new Query(sampleOfAllInstructionTypes);
74+
const query = new Query(sampleOfAllInstructionTypes, new TasksFile('sample.md'));
6975
expect(explainer.explainQuery(query)).toMatchInlineSnapshot(`
70-
"not done
76+
"filter by function \\
77+
task.path === '{{query.file.path}}'
78+
=>
79+
filter by function task.path === '{{query.file.path}}' =>
80+
filter by function task.path === 'sample.md'
81+
82+
not done
7183
7284
(has start date) AND (description includes some) =>
7385
AND (All of):
7486
has start date
7587
description includes some
7688
89+
group by function \\
90+
task.path.includes('{{query.file.path}}')
91+
=>
92+
group by function task.path.includes('{{query.file.path}}') =>
93+
group by function task.path.includes('sample.md')
94+
7795
group by priority reverse
96+
7897
group by happens
7998
99+
sort by function \\
100+
task.path.includes('{{query.file.path}}')
101+
=>
102+
sort by function task.path.includes('{{query.file.path}}') =>
103+
sort by function task.path.includes('sample.md')
104+
80105
sort by description reverse
106+
81107
sort by path
82108
83109
At most 50 tasks.
@@ -93,20 +119,40 @@ limit groups 3
93119
// Disable sort instructions
94120
updateSettings({ debugSettings: new DebugSettings(true) });
95121

96-
const query = new Query(sampleOfAllInstructionTypes);
122+
const query = new Query(sampleOfAllInstructionTypes, new TasksFile('sample.md'));
97123
const indentedExplainer = new Explainer(' ');
98124
expect(indentedExplainer.explainQuery(query)).toMatchInlineSnapshot(`
99-
" not done
125+
" filter by function \\
126+
task.path === '{{query.file.path}}'
127+
=>
128+
filter by function task.path === '{{query.file.path}}' =>
129+
filter by function task.path === 'sample.md'
130+
131+
not done
100132
101133
(has start date) AND (description includes some) =>
102134
AND (All of):
103135
has start date
104136
description includes some
105137
138+
group by function \\
139+
task.path.includes('{{query.file.path}}')
140+
=>
141+
group by function task.path.includes('{{query.file.path}}') =>
142+
group by function task.path.includes('sample.md')
143+
106144
group by priority reverse
145+
107146
group by happens
108147
148+
sort by function \\
149+
task.path.includes('{{query.file.path}}')
150+
=>
151+
sort by function task.path.includes('{{query.file.path}}') =>
152+
sort by function task.path.includes('sample.md')
153+
109154
sort by description reverse
155+
110156
sort by path
111157
112158
At most 50 tasks.
@@ -178,7 +224,9 @@ describe('explain groupers', () => {
178224
const query = new Query(source);
179225
expect(explainer.explainGroups(query)).toMatchInlineSnapshot(`
180226
"group by due
227+
181228
group by status.name reverse
229+
182230
group by function task.description.toUpperCase()
183231
"
184232
`);
@@ -199,21 +247,39 @@ describe('explain groupers', () => {
199247
];
200248
const query = makeQueryFromContinuationLines(lines);
201249

202-
// TODO Make this also show the original instruction, including continuations. See #2349.
203250
expect(explainer.explainGroups(query)).toMatchInlineSnapshot(`
204-
"group by function const date = task.due; if (!date.moment) { return "Undated"; } if (date.moment.day() === 0) { return date.format("[%%][8][%%]dddd"); } return date.format("[%%]d[%%]dddd");
251+
"group by function \\
252+
const date = task.due; \\
253+
if (!date.moment) { \\
254+
return "Undated"; \\
255+
} \\
256+
if (date.moment.day() === 0) { \\
257+
{{! Put the Sunday group last: }} \\
258+
return date.format("[%%][8][%%]dddd"); \\
259+
} \\
260+
return date.format("[%%]d[%%]dddd");
261+
=>
262+
group by function const date = task.due; if (!date.moment) { return "Undated"; } if (date.moment.day() === 0) { {{! Put the Sunday group last: }} return date.format("[%%][8][%%]dddd"); } return date.format("[%%]d[%%]dddd"); =>
263+
group by function const date = task.due; if (!date.moment) { return "Undated"; } if (date.moment.day() === 0) { return date.format("[%%][8][%%]dddd"); } return date.format("[%%]d[%%]dddd");
205264
"
206265
`);
266+
expect(query.grouping[0].instruction).toEqual(
267+
'group by function const date = task.due; if (!date.moment) { return "Undated"; } if (date.moment.day() === 0) { return date.format("[%%][8][%%]dddd"); } return date.format("[%%]d[%%]dddd");',
268+
);
207269
});
208270
});
209271

210272
describe('explain sorters', () => {
211273
it('should explain "sort by" options', () => {
212274
const source = 'sort by due\nsort by priority()';
213275
const query = new Query(source);
276+
// This shows the accidental presence of stray () characters after 'sort by priority'.
277+
// They are not *required* in the explanation, but are retained here to help in user support
278+
// when I ask users to supply an explanation of their query.
214279
expect(explainer.explainSorters(query)).toMatchInlineSnapshot(`
215280
"sort by due
216-
sort by priority
281+
282+
sort by priority()
217283
"
218284
`);
219285
});
@@ -229,11 +295,20 @@ describe('explain sorters', () => {
229295
];
230296
const query = makeQueryFromContinuationLines(lines);
231297

232-
// TODO Make this also show the original instruction, including continuations. See #2349.
233298
expect(explainer.explainSorters(query)).toMatchInlineSnapshot(`
234-
"sort by function const priorities = [..."🟥🟧🟨🟩🟦"]; for (let i = 0; i < priorities.length; i++) { if (task.description.includes(priorities[i])) return i; } return 999;
299+
"sort by function \\
300+
const priorities = [..."🟥🟧🟨🟩🟦"]; \\
301+
for (let i = 0; i < priorities.length; i++) { \\
302+
if (task.description.includes(priorities[i])) return i; \\
303+
} \\
304+
return 999;
305+
=>
306+
sort by function const priorities = [..."🟥🟧🟨🟩🟦"]; for (let i = 0; i < priorities.length; i++) { if (task.description.includes(priorities[i])) return i; } return 999;
235307
"
236308
`);
309+
expect(query.sorting[0].instruction).toEqual(
310+
'sort by function const priorities = [..."🟥🟧🟨🟩🟦"]; for (let i = 0; i < priorities.length; i++) { if (task.description.includes(priorities[i])) return i; } return 999;',
311+
);
237312
});
238313
});
239314

tests/Query/Group/Grouper.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Grouper, type GrouperFunction } from '../../../src/Query/Group/Grouper';
2+
import type { Task } from '../../../src/Task/Task';
3+
import type { SearchInfo } from '../../../src/Query/SearchInfo';
4+
import { Statement } from '../../../src/Query/Statement';
5+
6+
describe('Grouper', () => {
7+
const grouperFunction: GrouperFunction = (task: Task, _searchInfo: SearchInfo) => {
8+
return [task.lineNumber.toString()];
9+
};
10+
11+
it('should supply the original instruction', () => {
12+
const grouper = new Grouper('group by lineNumber', 'lineNumber', grouperFunction, false);
13+
14+
expect(grouper.instruction).toBe('group by lineNumber');
15+
expect(grouper.statement.rawInstruction).toBe('group by lineNumber');
16+
});
17+
18+
it('should store a Statement object', () => {
19+
const instruction = 'group by lineNumber';
20+
const statement = new Statement(instruction, instruction);
21+
const grouper = new Grouper('group by lineNumber', 'lineNumber', grouperFunction, false);
22+
23+
grouper.setStatement(statement);
24+
25+
expect(grouper.statement.rawInstruction).toBe('group by lineNumber');
26+
});
27+
});

tests/Query/Sort/Sorter.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type Comparator, Sorter } from '../../../src/Query/Sort/Sorter';
2+
import type { Task } from '../../../src/Task/Task';
3+
import type { SearchInfo } from '../../../src/Query/SearchInfo';
4+
import { Statement } from '../../../src/Query/Statement';
5+
6+
describe('Sorter', () => {
7+
const comparator: Comparator = (a: Task, b: Task, _searchInfo: SearchInfo) => {
8+
return a.lineNumber - b.lineNumber;
9+
};
10+
11+
it('should supply the original instruction', () => {
12+
const sorter = new Sorter('sort by lineNumber', 'lineNumber', comparator, false);
13+
14+
expect(sorter.instruction).toBe('sort by lineNumber');
15+
expect(sorter.statement.rawInstruction).toBe('sort by lineNumber');
16+
});
17+
18+
it('should store a Statement object', () => {
19+
const instruction = 'sort by lineNumber';
20+
const statement = new Statement(instruction, instruction);
21+
const sorter = new Sorter('sort by lineNumber', 'lineNumber', comparator, false);
22+
23+
sorter.setStatement(statement);
24+
25+
expect(sorter.statement.rawInstruction).toBe('sort by lineNumber');
26+
});
27+
});

0 commit comments

Comments
 (0)