vscode-todo-plus
Version:
Manage todo lists with ease. Powerful, easy to use and customizable.
233 lines (138 loc) • 5.74 kB
text/typescript
/* IMPORT */
import * as _ from 'lodash';
import * as querystring from 'querystring';
import * as vscode from 'vscode';
import Config from '../../../config';
import EmbeddedView from '../../../views/embedded';
import Folder from '../../folder';
/* ABSTRACT */
class Abstract {
include = undefined;
exclude = undefined;
rootPaths = undefined;
filesData = undefined; // { [filePath]: todo[] | undefined }
watcher: vscode.FileSystemWatcher = undefined;
async get ( rootPaths = Folder.getAllRootPaths (), groupByRoot = true, groupByType = true, groupByFile = true, filter: string | false = false ) {
rootPaths = _.castArray ( rootPaths );
const config = Config.get ();
this.include = config.embedded.include;
this.exclude = config.embedded.exclude;
if ( !this.filesData || !_.isEqual ( this.rootPaths, rootPaths ) ) {
this.rootPaths = rootPaths;
this.unwatchPaths ();
await this.initFilesData ( rootPaths );
this.watchPaths ();
} else {
await this.updateFilesData ();
}
return this.getTodos ( groupByRoot, groupByType, groupByFile, filter );
}
async watchPaths () {
/* HELPERS */
const pathNormalizer = filePath => filePath.replace ( /\\/g, '/' );
/* HANDLERS */
const refresh = _.debounce ( () => EmbeddedView.refresh (), 250 );
const add = event => {
if ( !this.filesData ) return;
const filePath = pathNormalizer ( event.fsPath );
if ( this.filesData.hasOwnProperty ( filePath ) ) return;
if ( !this.isIncluded ( filePath ) ) return;
this.filesData[filePath] = undefined;
refresh ();
};
const change = event => {
if ( !this.filesData ) return;
const filePath = pathNormalizer ( event.fsPath );
if ( !this.isIncluded ( filePath ) ) return;
this.filesData[filePath] = undefined;
refresh ();
};
const unlink = event => {
if ( !this.filesData ) return;
const filePath = pathNormalizer ( event.fsPath );
delete this.filesData[filePath];
refresh ();
};
/* WATCHING */
this.include.forEach ( glob => {
this.watcher = vscode.workspace.createFileSystemWatcher ( glob );
this.watcher.onDidCreate ( add );
this.watcher.onDidChange ( change );
this.watcher.onDidDelete ( unlink );
});
}
unwatchPaths () {
if ( !this.watcher ) return;
this.watcher.dispose ();
}
getIncluded ( filePaths ) {
const micromatch = require ( 'micromatch' ); // Lazy import for performance
return micromatch ( filePaths, this.include, { ignore: this.exclude, dot: true } );
}
isIncluded ( filePath ) {
return !!this.getIncluded ([ filePath ]).length;
}
async initFilesData ( rootPaths ) {
this.filesData = {};
}
async updateFilesData () {}
getTodos ( groupByRoot, groupByType, groupByFile, filter ) {
if ( _.isEmpty ( this.filesData ) ) return;
const todos = {}, // { [ROOT] { [TYPE] => { [FILEPATH] => [DATA] } } }
filterRe = filter ? new RegExp ( _.escapeRegExp ( filter ), 'i' ) : false,
filePaths = Object.keys ( this.filesData );
filePaths.forEach ( filePath => {
const data = this.filesData[filePath];
if ( !data || !data.length ) return;
const filePathGroup = groupByFile ? filePath : '';
data.forEach ( datum => {
if ( filterRe && !filterRe.test ( datum.line ) ) return;
const rootGroup = groupByRoot ? datum.root : '';
if ( !todos[rootGroup] ) todos[rootGroup] = {};
const typeGroup = groupByType ? datum.type : '';
if ( !todos[rootGroup][typeGroup] ) todos[rootGroup][typeGroup] = {};
if ( !todos[rootGroup][typeGroup][filePathGroup] ) todos[rootGroup][typeGroup][filePathGroup] = [];
todos[rootGroup][typeGroup][filePathGroup].push ( datum );
});
});
const roots = Object.keys ( todos );
return roots.length > 1 ? todos : { '': todos[roots[0]] };
}
renderTodos ( todos ) {
if ( _.isEmpty ( todos ) ) return '';
const sepRe = new RegExp ( querystring.escape ( '/' ), 'g' ),
config = Config.get (),
{ indentation, embedded: { file: { wholeLine } }, symbols: { box } } = config,
lines = [];
/* LINES */
const roots = Object.keys ( todos ).sort ();
roots.forEach ( root => {
if ( root ) {
lines.push ( `\n${root}:` );
}
const types = Object.keys ( todos[root] ).sort ();
types.forEach ( type => {
if ( type ) {
lines.push ( `${root ? indentation : '\n'}${type}:` );
}
const filePaths = Object.keys ( todos[root][type] ).sort ();
filePaths.forEach ( filePath => {
if ( filePath ) {
const normalizedFilePath = `/${_.trimStart ( filePath, '/' )}`,
encodedFilePath = querystring.escape ( normalizedFilePath ).replace ( sepRe, '/' );
lines.push ( `${root ? indentation : ''}${type ? indentation : ''}//${encodedFilePath}` );
}
const data = todos[root][type][filePath];
data.forEach ( datum => {
const normalizedFilePath = `/${_.trimStart ( datum.filePath, '/' )}`,
encodedFilePath = querystring.escape ( normalizedFilePath ).replace ( sepRe, '/' );
lines.push ( `${root ? indentation : ''}${type ? indentation : ''}${filePath ? indentation : ''}${box} ${_.trimStart ( wholeLine ? datum.line : datum.message )} //${encodedFilePath}#${datum.lineNr + 1}` );
});
});
});
});
return lines.length ? `${lines.join ( '\n' )}\n` : '';
}
}
/* EXPORT */
export default Abstract;