Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
- Added optional whole-line background colors for configured special tags with `todo.colors.tags.lineBackground`. Fixes: https://github.com/vycdev/vscode-todo-plus-two/issues/93
- Added TextMate syntax scopes so Todo projects, standard statuses, tags, and formatting are visible in the editor minimap. Fixes: https://github.com/vycdev/vscode-todo-plus-two/issues/70
- Added a command to copy the project at the cursor with its rendered statistics included as text. Fixes: https://github.com/vycdev/vscode-todo-plus-two/issues/73
- Added tag-scoped statistics tokens for todo counts, completion, estimates, and elapsed time, including AND/OR tag selectors. Fixes: https://github.com/vycdev/vscode-todo-plus-two/issues/81
- Fixed invalid statistics visibility expressions so they hide the affected statistics instead of interrupting extension updates.
- Fixed the Due view so `todo.due.soonDays: 0` keeps future tasks out of the soon window.
- Added a pending-task badge to the Todo activity bar icon on supported VS Code versions. Fixes: https://github.com/vycdev/vscode-todo-plus-two/issues/90
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ The following tokens can be used in `todo.statistics.project.text`, `todo.statis
| `[wasted]` | Time wasted on task |
| `[elapsed]` | Sum of `[lasted]` and `[wasted]` |

Tag-scoped tokens use `[tag:selector:metric]`. The metric can be any task or time token
from the table, and defaults to `all`, so `[tag:high]` counts todos tagged `@high` while
`[tag:high:elapsed]` reports their elapsed time. Join tag names with `&` to require every
tag or `|` to accept any tag: `[tag:frontend&high:pending]` and
`[tag:frontend|backend:done]`. `&` is evaluated before `|`, the leading `@` is optional,
and tag arguments do not affect matching (`@high(review)` matches `high`). These tokens
work in project text, status bar text and status bar tooltips.

## Demo

### [Example todo file](https://github.com/vycdev/vscode-todo-plus-two/blob/master/resources/readme.todo)
Expand Down
33 changes: 33 additions & 0 deletions src/utils/statistics-template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
const timeTokens = new Set(['est', 'est-total', 'est-finished', 'lasted', 'wasted', 'elapsed']);

const tagMetrics = new Set([
'tags',
'pending',
'done',
'cancelled',
'finished',
'all',
'percentage',
'est',
'est-total',
'lasted',
'wasted',
'elapsed',
'est-finished',
'est-finished-percentage',
]);

const tagTokenRegex = /\[tag:([^\]:\s]+)(?::([a-z-]+))?\]/g;

const tokenRegexes: { [token: string]: RegExp } = {};

const getTokenRegex = (token: string): RegExp => {
Expand Down Expand Up @@ -29,5 +48,19 @@ export const renderStatisticsTemplate = (
template = template.replace(regex, value);
}

if (typeof tokens.getTagMetric === 'function') {
template = template.replace(tagTokenRegex, (placeholder, selector, requestedMetric) => {
const metric = requestedMetric || 'all';

if (!tagMetrics.has(metric)) return placeholder;

let value = tokens.getTagMetric(selector, metric);

if (timeTokens.has(metric) && value === '') value = '0s';

return value;
});
}

return template;
};
68 changes: 66 additions & 2 deletions src/utils/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ const Statistics = {
]; // Global settings where tokens could be in use

Statistics.tokens.updateDisabled(Statistics.tokens.disabled.global, tokens, globalSettings);
Statistics.tokens.disabled.global.tagged = !globalSettings.some((setting) =>
_.includes(Config.getKey(setting), '[tag:')
);

const projectsSettings = ['statistics.project.enabled', 'statistics.project.text']; // Local settings where tokens could be in use

Expand All @@ -122,6 +125,9 @@ const Statistics = {
tokens,
projectsSettings
);
Statistics.tokens.disabled.projects.tagged = !projectsSettings.some((setting) =>
_.includes(Config.getKey(setting), '[tag:')
);
},

updateDisabled(obj, tokens: string[], settings: string[]) {
Expand All @@ -137,6 +143,28 @@ const Statistics = {

global: {},

createTaggedTodo(item) {
const todo = new Tokens();

if (item instanceof TodoBox) {
todo.pending = 1;
} else if (item instanceof TodoDone) {
todo.done = 1;
} else if (item instanceof TodoCancelled) {
todo.cancelled = 1;
} else {
return;
}

return todo;
},

addTaggedTodoTag(tag: Tag, todo: Tokens, disabledTokens) {
todo.tags++;
todo.tagNames.push(tag.text.replace(/^@/, '').replace(/\([^)]*\)$/, ''));
Statistics.timeTags.add(tag.text, todo, disabledTokens, todo.pending === 0);
},

updateGlobal(items) {
if (items.archive && Config.getKey('statistics.statusbar.ignoreArchive')) {
// Keeping only items before the archive
Expand Down Expand Up @@ -173,7 +201,8 @@ const Statistics = {

const lines = getStatisticsLines(items);

let wasPending = false;
let wasPending = false,
taggedTodo: Tokens | undefined;

for (let i = 0, l = lines.length; i < l; i++) {
const nextItem: any = lines[i];
Expand All @@ -186,13 +215,30 @@ const Statistics = {
Statistics.tokens.disabled.global,
!wasPending
);

if (taggedTodo) {
Statistics.tokens.addTaggedTodoTag(
nextItem,
taggedTodo,
Statistics.tokens.disabled.global
);
}
} else {
if (taggedTodo) tokens.taggedTodos.push(taggedTodo);

taggedTodo =
Statistics.tokens.disabled.global.tagged === true
? undefined
: Statistics.tokens.createTaggedTodo(nextItem);

// Only tags attached to a pending todo contribute remaining estimates.
// Comments, projects and finished todos all end the pending context.
wasPending = nextItem instanceof TodoBox;
}
}

if (taggedTodo) tokens.taggedTodos.push(taggedTodo);

Statistics.tokens.global = tokens;
},

Expand Down Expand Up @@ -244,7 +290,8 @@ const Statistics = {
includeRemainingDocument
);

let wasPending = false;
let wasPending = false,
taggedTodo: Tokens | undefined;

for (let i = lineNr + 1; i < scopeEnd; i++) {
const nextItem = lines[i];
Expand All @@ -258,7 +305,21 @@ const Statistics = {
Statistics.tokens.disabled.projects,
!wasPending
);

if (taggedTodo) {
Statistics.tokens.addTaggedTodoTag(
nextItem,
taggedTodo,
Statistics.tokens.disabled.projects
);
}
} else {
if (taggedTodo) tokens.taggedTodos.push(taggedTodo);

taggedTodo =
Statistics.tokens.disabled.projects.tagged === true
? undefined
: Statistics.tokens.createTaggedTodo(nextItem);
nextItem.level = nextItem.level || AST.getLevel(textDocument, nextItem.line.text);

wasPending = nextItem instanceof TodoBox;
Expand All @@ -282,6 +343,7 @@ const Statistics = {
tokens.estTotalSeconds += nextTokens.estTotalSeconds;
tokens.lastedSeconds += nextTokens.lastedSeconds;
tokens.wastedSeconds += nextTokens.wastedSeconds;
tokens.taggedTodos.push(...nextTokens.taggedTodos);

i +=
nextTokens.comments +
Expand All @@ -303,6 +365,8 @@ const Statistics = {
}
}

if (taggedTodo) tokens.taggedTodos.push(taggedTodo);

Statistics.tokens.projects[project.lineNumber] = tokens;

return tokens;
Expand Down
34 changes: 34 additions & 0 deletions src/utils/statistics_tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,40 @@ class StatisticsTokens {
estTotalSeconds = 0;
lastedSeconds = 0;
wastedSeconds = 0;
tagNames: string[] = [];
taggedTodos: StatisticsTokens[] = [];

getTagMetric(selector: string, metric = 'all') {
return this.getTagTokens(selector)[metric];
}

getTagTokens(selector: string) {
const clauses = selector.split('|').map((clause) =>
clause
.split('&')
.map((tag) => tag.replace(/^@/, ''))
.filter(Boolean)
),
matching = this.taggedTodos.filter((todo) =>
clauses.some(
(clause) => clause.length && clause.every((tag) => todo.tagNames.indexOf(tag) >= 0)
)
),
tokens = new StatisticsTokens();

matching.forEach((todo) => {
tokens.tags += todo.tags;
tokens.pending += todo.pending;
tokens.done += todo.done;
tokens.cancelled += todo.cancelled;
tokens.estSeconds += todo.estSeconds;
tokens.estTotalSeconds += todo.estTotalSeconds;
tokens.lastedSeconds += todo.lastedSeconds;
tokens.wastedSeconds += todo.wastedSeconds;
});

return tokens;
}

@memoize
get finished() {
Expand Down
Loading
Loading