11import { type App , type Component , Notice , type TFile } from 'obsidian' ;
2- import { GlobalFilter } from '../Config/GlobalFilter' ;
3- import { GlobalQuery } from '../Config/GlobalQuery' ;
42import { postponeButtonTitle , shouldShowPostponeButton } from '../DateTime/Postponer' ;
5- import type { IQuery } from '../IQuery' ;
63import { QueryLayout } from '../Layout/QueryLayout' ;
74import { TaskLayout } from '../Layout/TaskLayout' ;
8- import { PerformanceTracker } from '../lib/PerformanceTracker' ;
9- import { State } from '../Obsidian/Cache' ;
105import type { GroupDisplayHeading } from '../Query/Group/GroupDisplayHeading' ;
11- import type { TaskGroups } from '../Query/Group/TaskGroups' ;
12- import { explainResults } from '../Query/QueryRendererHelper' ;
136import type { QueryResult } from '../Query/QueryResult' ;
14- import type { TasksFile } from '../Scripting/TasksFile' ;
157import type { ListItem } from '../Task/ListItem' ;
16- import { Task } from '../Task/Task' ;
8+ import type { Task } from '../Task/Task' ;
179import { PostponeMenu } from '../ui/Menus/PostponeMenu' ;
1810import { showMenu } from '../ui/Menus/TaskEditingMenu' ;
11+ import type { TaskGroup } from '../Query/Group/TaskGroup' ;
1912import type { QueryRendererParameters } from './QueryResultsRenderer' ;
2013import { 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