UNPKG

vscode-todo-plus

Version:

Manage todo lists with ease. Powerful, easy to use and customizable.

337 lines (185 loc) 8.72 kB
/* IMPORT */ import * as _ from 'lodash'; import * as vscode from 'vscode'; import Config from '../config'; import Consts from '../consts'; import {Comment, Project, Tag, TodoBox, TodoDone, TodoCancelled} from '../todo/items'; import AST from './ast'; import Tokens from './statistics_tokens'; import Time from './time'; /* STATISTICS */ const Statistics = { /* TIME TAGS */ timeTags: { add ( tag: string, tokens: Tokens, disabledTokens, disabledEst = false ) { const prefix = tag[1]; if ( !disabledTokens.lasted && prefix === 'l' ) { // Maybe @lasted(2h) tokens.lastedSeconds += Statistics.timeTags.parseElapsed ( tag ); } else if ( !disabledTokens.wasted && prefix === 'w' ) { // maybe @wasted(30m) tokens.wastedSeconds += Statistics.timeTags.parseElapsed ( tag ); } else if ( !disabledTokens.est && !disabledEst && ( prefix === 'e' || ( prefix >= '0' && prefix <= '9' ) ) ) { // Maybe @est(1h20m) or @1h20m tokens.estSeconds += Statistics.timeTags.parseEstimate ( tag ); } }, elapseds: {}, parseElapsed ( tag: string ) { if ( Statistics.timeTags.elapseds[tag] ) return Statistics.timeTags.elapseds[tag]; const match = tag.match ( Consts.regexes.tagElapsed ); if ( !match ) return 0; const time = match[1], seconds = Time.diffSeconds ( time ); Statistics.timeTags.elapseds[tag] = seconds; return seconds; }, estimates: {}, // It assumes that all estimates are relative to `now` parseEstimate ( tag: string, from?: Date ) { if ( Statistics.timeTags.estimates[tag] ) return Statistics.timeTags.estimates[tag]; const est = tag.match ( Consts.regexes.tagEstimate ); if ( !est ) return 0; const time = est[2] || est[1], seconds = Time.diffSeconds ( time, from ); Statistics.timeTags.estimates[tag] = seconds; return seconds; } }, /* CONDITION */ condition: { functions: {}, // Cache of functions created from conditions toFunction ( condition ) { // Avoiding repeatedly calling `eval` if ( Statistics.condition.functions[condition] ) return Statistics.condition.functions[condition]; const fn = new Function ( 'global', 'project', `return ${condition}` ); Statistics.condition.functions[condition] = fn; return fn; }, is ( condition, globalTokens, projectTokens ) { if ( _.isBoolean ( condition ) ) return condition; if ( !globalTokens && !projectTokens ) return false; const fn = Statistics.condition.toFunction ( condition ); try { return !!fn ( globalTokens, projectTokens ); } catch ( e ) { return false; } } }, /* TOKENS */ tokens: { disabled: { // Disabled tokens, no need to compute them global: {}, projects: {} }, updateDisabledAll () { const tokens = ['est', 'lasted', 'wasted', 'elapsed']; // These are the expensive tokens const globalSettings = ['statistics.statusbar.enabled', 'statistics.statusbar.text', 'statistics.statusbar.tooltip']; // Global settings where tokens could be in use Statistics.tokens.updateDisabled ( Statistics.tokens.disabled.global, tokens, globalSettings ); const projectsSettings = ['statistics.project.enabled', 'statistics.project.text']; // Local settings where tokens could be in use Statistics.tokens.updateDisabled ( Statistics.tokens.disabled.projects, tokens, projectsSettings ); }, updateDisabled ( obj, tokens: string[], settings: string[] ) { // Ugly name, tokens.forEach ( token => { obj[token] = !settings.find ( setting => _.includes ( Config.getKey ( setting ), token ) ); }); }, global: {}, updateGlobal ( items ) { if ( items.archive && Config.getKey ( 'statistics.statusbar.ignoreArchive' ) ) { // Keeping only items before the archive items = _.reduce ( items, ( acc, value, key ) => { const newValue = _.isArray ( value ) ? value.filter ( item => item.lineNumber < items.archive.lineNumber ) : value; acc[key] = newValue; return acc; }, {} ); } const tokens = _.extend ( new Tokens (), { comments: items.comments.length, projects: items.projects.length, tags: items.tags.length, pending: items.todosBox.length, done: items.todosDone.length, cancelled: items.todosCancelled.length }); items.tags.forEach ( tag => Statistics.timeTags.add ( tag.text, tokens, Statistics.tokens.disabled.global ) ); Statistics.tokens.global = tokens; }, projects: {}, updateProjects ( textDocument: vscode.TextDocument, items ) { Statistics.tokens.projects = {}; if ( !items.projects ) return; function mergeSorted ( arr1, arr2 ) { //URL: https://stackoverflow.com/questions/5958169/how-to-merge-two-sorted-arrays-into-a-sorted-array#answer-31310853 const merged = new Array ( arr1.length + arr2.length ); let i = arr1.length - 1, j = arr2.length - 1, k = merged.length; while ( k ) { merged[--k] = ( j < 0 || ( i >= 0 && arr1[i].lineNumber > arr2[j].lineNumber ) ) ? arr1[i--] : arr2[j--]; } return merged; } const groups = [items.projects, items.todosBox, items.todosDone, items.todosCancelled, items.tags.length >= 500 ? [] : items.tags], //TODO: Undocumented, uncustomizable limitation regarding tags lines = groups.reduce ( ( arr1, arr2 ) => mergeSorted ( arr1, arr2 ) ); items.projects.forEach ( project => { Statistics.tokens.updateProject ( textDocument, project, lines, lines.indexOf ( project ) ); }); }, updateProject ( textDocument: vscode.TextDocument, project, lines, lineNr: 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 (); let wasPending = false; for ( let i = lineNr + 1, l = lines.length; i < l; i++ ) { const nextItem = lines[i]; if ( nextItem instanceof Tag ) { tokens.tags++; Statistics.timeTags.add ( nextItem.text, tokens, Statistics.tokens.disabled.projects, !wasPending ); } else { nextItem.level = ( nextItem.level || AST.getLevel ( textDocument, nextItem.line.text ) ); if ( nextItem.level <= project.level ) break; wasPending = nextItem instanceof TodoBox; if ( nextItem instanceof Project ) { const nextTokens = Statistics.tokens.updateProject ( textDocument, nextItem, lines, i ); tokens.comments += nextTokens.comments; tokens.projects += 1 + nextTokens.projects; tokens.tags += nextTokens.tags; tokens.pending += nextTokens.pending; tokens.done += nextTokens.done; tokens.cancelled += nextTokens.cancelled; tokens.estSeconds += nextTokens.estSeconds; tokens.lastedSeconds += nextTokens.lastedSeconds; tokens.wastedSeconds += nextTokens.wastedSeconds; i += nextTokens.comments + nextTokens.projects + nextTokens.tags + nextTokens.pending + nextTokens.done + nextTokens.cancelled; // Jumping } if ( nextItem instanceof Comment ) { tokens.comments++; } else if ( nextItem instanceof TodoBox ) { tokens.pending++; } else if ( nextItem instanceof TodoDone ) { tokens.done++; } else if ( nextItem instanceof TodoCancelled ) { tokens.cancelled++; } } } Statistics.tokens.projects[project.lineNumber] = tokens; return tokens; } }, /* TEMPLATE */ template: { tokensRe: {}, // Map of `token => tokenRe` getTokenRe ( token ) { if ( Statistics.template.tokensRe[token] ) return Statistics.template.tokensRe[token]; const re = new RegExp ( `\\[${_.escapeRegExp ( token )}\\]`, 'g' ); Statistics.template.tokensRe[token] = re; return re; }, render ( template: string, tokens = Statistics.getTokens () ) { if ( !tokens ) return; for ( let token of Tokens.supported ) { const re = Statistics.template.getTokenRe ( token ); if ( !re.test ( template ) ) continue; template = template.replace ( re, tokens[token] ); } return template; } } }; /* EXPORT */ export default Statistics;