Skip to content

Commit 39b4f33

Browse files
authored
Merge pull request #3675 from obsidian-tasks-group/refactor-QueryResultsRendererBase
refactor: extract `QueryResultsRendererBase` class
2 parents 9b789c8 + e3febc7 commit 39b4f33

File tree

2 files changed

+257
-205
lines changed

2 files changed

+257
-205
lines changed

src/Renderer/HtmlQueryResultsRenderer.ts

Lines changed: 27 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,33 @@
11
import { type App, type Component, Notice, type TFile } from 'obsidian';
2-
import { GlobalFilter } from '../Config/GlobalFilter';
3-
import { GlobalQuery } from '../Config/GlobalQuery';
42
import { postponeButtonTitle, shouldShowPostponeButton } from '../DateTime/Postponer';
5-
import type { IQuery } from '../IQuery';
63
import { QueryLayout } from '../Layout/QueryLayout';
74
import { TaskLayout } from '../Layout/TaskLayout';
8-
import { PerformanceTracker } from '../lib/PerformanceTracker';
9-
import { State } from '../Obsidian/Cache';
105
import type { GroupDisplayHeading } from '../Query/Group/GroupDisplayHeading';
11-
import type { TaskGroups } from '../Query/Group/TaskGroups';
12-
import { explainResults } from '../Query/QueryRendererHelper';
136
import type { QueryResult } from '../Query/QueryResult';
14-
import type { TasksFile } from '../Scripting/TasksFile';
157
import type { ListItem } from '../Task/ListItem';
16-
import { Task } from '../Task/Task';
8+
import type { Task } from '../Task/Task';
179
import { PostponeMenu } from '../ui/Menus/PostponeMenu';
1810
import { showMenu } from '../ui/Menus/TaskEditingMenu';
11+
import type { TaskGroup } from '../Query/Group/TaskGroup';
1912
import type { QueryRendererParameters } from './QueryResultsRenderer';
2013
import { TaskLineRenderer, type TextRenderer, createAndAppendElement } from './TaskLineRenderer';
14+
import { QueryResultsRendererBase, type QueryResultsRendererGetters } from './QueryResultsRendererBase';
2115

22-
/**
23-
* Because properties in QueryResultsRenderer may be modified during the lifetime of this class,
24-
* we pass in getter functions instead of storing duplicate copies of the values.
25-
*/
26-
interface QueryResultsRendererGetters {
27-
source: () => string;
28-
tasksFile: () => TasksFile;
29-
query: () => IQuery;
30-
}
31-
32-
export class HtmlQueryResultsRenderer {
16+
export class HtmlQueryResultsRenderer extends QueryResultsRendererBase {
3317
// Renders the description in TaskLineRenderer:
3418
protected readonly textRenderer;
3519

3620
// Renders the group heading in this class:
3721
protected readonly renderMarkdown;
3822
protected readonly obsidianComponent: Component | null;
3923
protected readonly obsidianApp: App;
40-
public getters: QueryResultsRendererGetters;
4124

4225
// TODO access this via getContent() for now
4326
public content: HTMLDivElement | null = null;
4427

4528
private readonly taskLineRenderer: TaskLineRenderer;
4629

4730
private readonly ulElementStack: HTMLUListElement[] = [];
48-
private readonly addedListItems: Set<ListItem> = new Set<ListItem>();
4931

5032
private readonly queryRendererParameters: QueryRendererParameters;
5133

@@ -63,11 +45,12 @@ export class HtmlQueryResultsRenderer {
6345
queryRendererParameters: QueryRendererParameters,
6446
getters: QueryResultsRendererGetters,
6547
) {
48+
super(getters);
49+
6650
this.renderMarkdown = renderMarkdown;
6751
this.obsidianComponent = obsidianComponent;
6852
this.obsidianApp = obsidianApp;
6953
this.textRenderer = textRenderer;
70-
this.getters = getters;
7154
this.queryRendererParameters = queryRendererParameters;
7255

7356
this.taskLineRenderer = new TaskLineRenderer({
@@ -79,24 +62,6 @@ export class HtmlQueryResultsRenderer {
7962
});
8063
}
8164

82-
public get filePath(): string | undefined {
83-
return this.getters.tasksFile().path;
84-
}
85-
86-
public async renderQuery(state: State | State.Warm, tasks: Task[]) {
87-
// Don't log anything here, for any state, as it generates huge amounts of
88-
// console messages in large vaults, if Obsidian was opened with any
89-
// notes with tasks code blocks in Reading or Live Preview mode.
90-
const error = this.getters.query().error;
91-
if (state === State.Warm && error === undefined) {
92-
await this.renderQuerySearchResults(tasks, state);
93-
} else if (error) {
94-
this.renderErrorMessage(error);
95-
} else {
96-
this.renderLoadingMessage();
97-
}
98-
}
99-
10065
private getContent() {
10166
// TODO remove throw
10267
const content = this.content;
@@ -106,71 +71,27 @@ export class HtmlQueryResultsRenderer {
10671
return content;
10772
}
10873

109-
private async renderQuerySearchResults(tasks: Task[], state: State.Warm) {
110-
const queryResult = this.explainAndPerformSearch(state, tasks);
111-
112-
if (queryResult.searchErrorMessage !== undefined) {
113-
// There was an error in the search, for example due to a problem custom function.
114-
this.renderErrorMessage(queryResult.searchErrorMessage);
115-
return;
116-
}
117-
118-
await this.renderSearchResults(queryResult);
119-
}
120-
121-
private explainAndPerformSearch(state: State.Warm, tasks: Task[]) {
122-
const measureSearch = new PerformanceTracker(`Search: ${this.getters.query().queryId} - ${this.filePath}`);
123-
measureSearch.start();
124-
125-
this.getters.query().debug(`[render] Render called: plugin state: ${state}; searching ${tasks.length} tasks`);
126-
127-
if (this.getters.query().queryLayoutOptions.explainQuery) {
128-
this.renderExplanation();
129-
}
130-
131-
const queryResult = this.getters.query().applyQueryToTasks(tasks);
132-
133-
measureSearch.finish();
134-
return queryResult;
135-
}
136-
137-
private async renderSearchResults(queryResult: QueryResult) {
138-
const measureRender = new PerformanceTracker(`Render: ${this.getters.query().queryId} - ${this.filePath}`);
139-
measureRender.start();
140-
74+
protected renderSearchResultsHeader(queryResult: QueryResult): void {
14175
this.addCopyButton(queryResult);
76+
}
14277

143-
await this.addAllTaskGroups(queryResult.taskGroups);
144-
145-
const totalTasksCount = queryResult.totalTasksCount;
78+
protected renderSearchResultsFooter(queryResult: QueryResult): void {
14679
this.addTaskCount(queryResult);
147-
148-
this.getters.query().debug(`[render] ${totalTasksCount} tasks displayed`);
149-
150-
measureRender.finish();
15180
}
15281

153-
private renderErrorMessage(errorMessage: string) {
82+
protected renderErrorMessage(errorMessage: string) {
15483
const container = createAndAppendElement('div', this.getContent());
15584
container.innerHTML = '<pre>' + `Tasks query: ${errorMessage.replace(/\n/g, '<br>')}` + '</pre>';
15685
}
15786

158-
private renderLoadingMessage() {
87+
protected renderLoadingMessage() {
15988
this.getContent().textContent = 'Loading Tasks ...';
16089
}
16190

162-
// Use the 'explain' instruction to enable this
163-
private renderExplanation() {
164-
const explanationAsString = explainResults(
165-
this.getters.source(),
166-
GlobalFilter.getInstance(),
167-
GlobalQuery.getInstance(),
168-
this.getters.tasksFile(),
169-
);
170-
91+
protected renderExplanation(explanation: string | null) {
17192
const explanationsBlock = createAndAppendElement('pre', this.getContent());
17293
explanationsBlock.classList.add('plugin-tasks-query-explanation');
173-
explanationsBlock.textContent = explanationAsString;
94+
explanationsBlock.textContent = explanation;
17495
}
17596

17697
private addCopyButton(queryResult: QueryResult) {
@@ -183,25 +104,18 @@ export class HtmlQueryResultsRenderer {
183104
});
184105
}
185106

186-
private async addAllTaskGroups(tasksSortedLimitedGrouped: TaskGroups) {
187-
for (const group of tasksSortedLimitedGrouped.groups) {
188-
// If there were no 'group by' instructions, group.groupHeadings
189-
// will be empty, and no headings will be added.
190-
await this.addGroupHeadings(group.groupHeadings);
191-
192-
this.addedListItems.clear();
193-
// TODO re-extract the method to include this back
194-
const taskList = createAndAppendElement('ul', this.getContent());
195-
this.ulElementStack.push(taskList);
196-
try {
197-
await this.addTaskList(group.tasks);
198-
} finally {
199-
this.ulElementStack.pop();
200-
}
107+
protected async addTaskGroup(group: TaskGroup): Promise<void> {
108+
// TODO re-extract the method to include this back
109+
const taskList = createAndAppendElement('ul', this.getContent());
110+
this.ulElementStack.push(taskList);
111+
try {
112+
await this.addTaskList(group.tasks);
113+
} finally {
114+
this.ulElementStack.pop();
201115
}
202116
}
203117

204-
private async addTaskList(listItems: ListItem[]): Promise<void> {
118+
protected beginTaskList(): void {
205119
const taskList = this.currentULElement();
206120
taskList.classList.add(
207121
'contains-task-list',
@@ -211,92 +125,12 @@ export class HtmlQueryResultsRenderer {
211125
);
212126

213127
const groupingAttribute = this.getGroupingAttribute();
214-
if (groupingAttribute && groupingAttribute.length > 0) taskList.dataset.taskGroupBy = groupingAttribute;
215-
216-
if (this.getters.query().queryLayoutOptions.hideTree) {
217-
await this.addFlatTaskList(listItems);
218-
} else {
219-
await this.addTreeTaskList(listItems);
220-
}
221-
}
222-
223-
/**
224-
* Old-style rendering of tasks:
225-
* - What is rendered:
226-
* - Only task lines that match the query are rendered, as a flat list
227-
* - The order that lines are rendered:
228-
* - Tasks are rendered in the order specified in 'sort by' instructions and default sort order.
229-
* @param listItems
230-
* @private
231-
*/
232-
private async addFlatTaskList(listItems: ListItem[]): Promise<void> {
233-
for (const [listItemIndex, listItem] of listItems.entries()) {
234-
if (listItem instanceof Task) {
235-
await this.addTask(listItem, listItemIndex, []);
236-
}
237-
}
238-
}
239-
240-
/** New-style rendering of tasks:
241-
* - What is rendered:
242-
* - Task lines that match the query are rendered, as a tree.
243-
* - Currently, all child tasks and list items of the found tasks are shown,
244-
* including any child tasks that did not match the query.
245-
* - The order that lines are rendered:
246-
* - The top-level/outermost tasks are sorted in the order specified in 'sort by'
247-
* instructions and default sort order.
248-
* - Child tasks (and list items) are shown in their original order in their Markdown file.
249-
* @param listItems
250-
* @private
251-
*/
252-
private async addTreeTaskList(listItems: ListItem[]): Promise<void> {
253-
for (const [listItemIndex, listItem] of listItems.entries()) {
254-
if (this.alreadyAdded(listItem)) {
255-
continue;
256-
}
257-
258-
if (this.willBeAddedLater(listItem, listItems)) {
259-
continue;
260-
}
261-
262-
if (listItem instanceof Task) {
263-
await this.addTask(listItem, listItemIndex, listItem.children);
264-
} else {
265-
await this.addListItem(listItem, listItemIndex, listItem.children);
266-
}
267-
268-
// The children of this item will be added thanks to recursion and the fact that we always render all children currently
269-
this.addedListItems.add(listItem);
270-
271-
// We think this code may be needed in future, we have been unable to write a failing test for it
272-
// for (const childTask of listItem.children) {
273-
// this.addedListItems.add(childTask);
274-
// }
275-
}
276-
}
277-
278-
private willBeAddedLater(listItem: ListItem, listItems: ListItem[]) {
279-
const closestParentTask = listItem.findClosestParentTask();
280-
if (!closestParentTask) {
281-
return false;
282-
}
283-
284-
if (!this.addedListItems.has(closestParentTask)) {
285-
// This task is a direct or indirect child of another task that we are waiting to draw,
286-
// so don't draw it yet, it will be done recursively later.
287-
if (listItems.includes(closestParentTask)) {
288-
return true;
289-
}
128+
if (groupingAttribute && groupingAttribute.length > 0) {
129+
taskList.dataset.taskGroupBy = groupingAttribute;
290130
}
291-
292-
return false;
293-
}
294-
295-
private alreadyAdded(listItem: ListItem) {
296-
return this.addedListItems.has(listItem);
297131
}
298132

299-
private async addListItem(listItem: ListItem, listItemIndex: number, children: ListItem[]): Promise<void> {
133+
protected async addListItem(listItem: ListItem, listItemIndex: number, children: ListItem[]): Promise<void> {
300134
const listItemElement = await this.taskLineRenderer.renderListItem(
301135
this.currentULElement(),
302136
listItem,
@@ -315,7 +149,7 @@ export class HtmlQueryResultsRenderer {
315149
}
316150
}
317151

318-
private async addTask(task: Task, taskIndex: number, children: ListItem[]): Promise<void> {
152+
protected async addTask(task: Task, taskIndex: number, children: ListItem[]): Promise<void> {
319153
const isFilenameUnique = this.isFilenameUnique({ task }, this.queryRendererParameters.allMarkdownFiles());
320154
const listItem = await this.taskLineRenderer.renderTaskLine({
321155
parentUlElement: this.currentULElement(),
@@ -386,19 +220,7 @@ export class HtmlQueryResultsRenderer {
386220
span.classList.add('tasks-urgency');
387221
}
388222

389-
/**
390-
* Display headings for a group of tasks.
391-
* @param groupHeadings - The headings to display. This can be an empty array,
392-
* in which case no headings will be added.
393-
* @private
394-
*/
395-
private async addGroupHeadings(groupHeadings: GroupDisplayHeading[]) {
396-
for (const heading of groupHeadings) {
397-
await this.addGroupHeading(heading);
398-
}
399-
}
400-
401-
private async addGroupHeading(group: GroupDisplayHeading) {
223+
protected async addGroupHeading(group: GroupDisplayHeading) {
402224
// Headings nested to 2 or more levels are all displayed with 'h6:
403225
let header: keyof HTMLElementTagNameMap = 'h6';
404226
if (group.nestingLevel === 0) {

0 commit comments

Comments
 (0)