From fa9e2e47a850116a802806a5f0241391dd395c38 Mon Sep 17 00:00:00 2001 From: vycdev2 Date: Mon, 10 Aug 2026 01:17:31 +0000 Subject: [PATCH] feat: add archive statistics --- CHANGELOG.md | 1 + src/utils/statistics-lines.ts | 17 +++++++++++++ src/utils/statistics.ts | 38 ++++++++++++++++++++------- test/statistics-lines.spec.ts | 48 ++++++++++++++++++++++++++++++++++- 4 files changed, 94 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72caf8d..f1ad5c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ - Fixed time tags on comments or project headers being counted as estimates for the preceding pending todo. - Fixed Todo and embedded file grouping so similarly prefixed sibling folders are not treated as workspace roots. - Preserved CRLF line endings when merging tasks into existing archive files. +- Added project statistics to the Archive header so estimates can be compared with elapsed time across archived tasks. Fixes: https://github.com/vycdev/vscode-todo-plus-two/issues/87 ### Version 5.2.0 diff --git a/src/utils/statistics-lines.ts b/src/utils/statistics-lines.ts index 11df570..2e235a6 100644 --- a/src/utils/statistics-lines.ts +++ b/src/utils/statistics-lines.ts @@ -11,6 +11,23 @@ interface StatisticsItems { tags?: T[]; } +export const getStatisticsScopeEnd = ( + lines: T[], + startIndex: number, + scopeLevel: number, + getLevel: (line: T) => number, + isTag: (line: T) => boolean, + includeRemainingDocument = false +): number => { + if (includeRemainingDocument) return lines.length; + + for (let index = startIndex + 1; index < lines.length; index++) { + if (!isTag(lines[index]) && getLevel(lines[index]) <= scopeLevel) return index; + } + + return lines.length; +}; + const mergeSorted = (left: T[], right: T[]): T[] => { const merged = new Array(left.length + right.length); diff --git a/src/utils/statistics.ts b/src/utils/statistics.ts index 2085b06..1fb8945 100644 --- a/src/utils/statistics.ts +++ b/src/utils/statistics.ts @@ -7,7 +7,7 @@ import Consts from '../consts'; import { Comment, Project, Tag, TodoBox, TodoDone, TodoCancelled } from '../todo/items'; import AST from './ast'; import { getEstimateDuration } from './estimate'; -import { getStatisticsLines } from './statistics-lines'; +import { getStatisticsLines, getStatisticsScopeEnd } from './statistics-lines'; import Tokens from './statistics_tokens'; import Time from './time'; @@ -224,29 +224,50 @@ const Statistics = { if (!items.projects) return; - const lines = getStatisticsLines(items); + const lines = getStatisticsLines(items), + archiveLineNumber = items.archive && items.archive.lineNumber; items.projects.forEach((project) => { Statistics.tokens.updateProject( textDocument, project, lines, - lines.indexOf(project) + lines.indexOf(project), + archiveLineNumber ); }); }, - updateProject(textDocument: vscode.TextDocument, project, lines, lineNr: number) { + updateProject( + textDocument: vscode.TextDocument, + project, + lines, + lineNr: number, + archiveLineNumber?: number + ) { if (Statistics.tokens.projects[project.lineNumber]) return Statistics.tokens.projects[project.lineNumber]; project.level = project.level || AST.getLevel(textDocument, project.line.text); - const tokens = new Tokens(); + const tokens = new Tokens(), + includeRemainingDocument = project.lineNumber === archiveLineNumber, + scopeEnd = getStatisticsScopeEnd( + lines, + lineNr, + project.level, + (item: any) => { + item.level = item.level || AST.getLevel(textDocument, item.line.text); + + return item.level; + }, + (item) => item instanceof Tag, + includeRemainingDocument + ); let wasPending = false; - for (let i = lineNr + 1, l = lines.length; i < l; i++) { + for (let i = lineNr + 1; i < scopeEnd; i++) { const nextItem = lines[i]; if (nextItem instanceof Tag) { @@ -262,8 +283,6 @@ const Statistics = { nextItem.level = nextItem.level || AST.getLevel(textDocument, nextItem.line.text); - if (nextItem.level <= project.level) break; - wasPending = nextItem instanceof TodoBox; if (nextItem instanceof Project) { @@ -271,7 +290,8 @@ const Statistics = { textDocument, nextItem, lines, - i + i, + archiveLineNumber ); tokens.comments += nextTokens.comments; diff --git a/test/statistics-lines.spec.ts b/test/statistics-lines.spec.ts index 03cf7a2..b4e2932 100644 --- a/test/statistics-lines.spec.ts +++ b/test/statistics-lines.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { getStatisticsLines } from '../src/utils/statistics-lines'; +import { getStatisticsLines, getStatisticsScopeEnd } from '../src/utils/statistics-lines'; describe('Statistics line ordering', () => { it('places each line context before tags on the same line', () => { @@ -47,4 +47,50 @@ describe('Statistics line ordering', () => { expect(tagStates).to.deep.equal([false]); }); + + it('stops normal project statistics at a peer project', () => { + const lines = [ + { type: 'project', level: 0 }, + { type: 'todo', level: 1 }, + { type: 'tag', level: 1 }, + { type: 'project', level: 0 }, + { type: 'todo', level: 1 }, + ], + end = getStatisticsScopeEnd( + lines, + 0, + 0, + (line) => line.level, + (line) => line.type === 'tag' + ); + + expect(lines.slice(1, end).map((line) => line.type)).to.deep.equal(['todo', 'tag']); + }); + + it('includes peer and nested projects through the end of an archive', () => { + const lines = [ + { type: 'archive', level: 0 }, + { type: 'todo', level: 0 }, + { type: 'project', level: 0 }, + { type: 'todo', level: 1 }, + { type: 'project', level: 1 }, + { type: 'todo', level: 2 }, + ], + end = getStatisticsScopeEnd( + lines, + 0, + 0, + (line) => line.level, + (line) => line.type === 'tag', + true + ); + + expect(lines.slice(1, end).map((line) => line.type)).to.deep.equal([ + 'todo', + 'project', + 'todo', + 'project', + 'todo', + ]); + }); });