├── .gitignore ├── icons ├── icon.png ├── dark │ ├── collapse-all.svg │ ├── close.svg │ ├── todo.svg │ ├── fixme.svg │ ├── refresh.svg │ └── hack.svg └── light │ ├── collapse-all.svg │ ├── close.svg │ ├── todo.svg │ ├── fixme.svg │ ├── refresh.svg │ └── hack.svg ├── images ├── gmail.gif ├── usage.gif ├── context.png ├── preview.png ├── trello.gif ├── trello.png ├── highlight.png └── insert-command.gif ├── .vscodeignore ├── src ├── functions │ ├── get-document-type.ts │ ├── register-command.ts │ ├── open-resource.ts │ ├── generate-comment.ts │ ├── edit-comment.ts │ ├── insert-comment.ts │ ├── remove-comment.ts │ ├── register-tree.ts │ ├── create-tree.ts │ ├── utils.ts │ └── read-comments.ts ├── models │ ├── action-comment-collection.ts │ └── action-comment.ts ├── consts.ts ├── modules │ ├── base.ts │ ├── uri-handler.ts │ ├── gmail.ts │ ├── decorator.ts │ ├── code-actions.ts │ ├── telemetry.ts │ ├── modifications.ts │ ├── github.ts │ └── trello.ts ├── config.ts ├── tree-provider.ts └── extension.ts ├── tsconfig.json ├── readme.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | out 3 | node_modules 4 | *.vsix -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/icons/icon.png -------------------------------------------------------------------------------- /images/gmail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/gmail.gif -------------------------------------------------------------------------------- /images/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/usage.gif -------------------------------------------------------------------------------- /images/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/context.png -------------------------------------------------------------------------------- /images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/preview.png -------------------------------------------------------------------------------- /images/trello.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/trello.gif -------------------------------------------------------------------------------- /images/trello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/trello.png -------------------------------------------------------------------------------- /images/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/highlight.png -------------------------------------------------------------------------------- /images/insert-command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tzachov/vscode-todo-list/HEAD/images/insert-command.gif -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /src/functions/get-document-type.ts: -------------------------------------------------------------------------------- 1 | export function getDocumentType(fsPath: string) { 2 | return (/\.([\w]+)$/.exec(fsPath) || [null]).pop(); 3 | } 4 | -------------------------------------------------------------------------------- /src/models/action-comment-collection.ts: -------------------------------------------------------------------------------- 1 | import { ActionComment } from "./action-comment"; 2 | 3 | export interface ActionCommentCollection { 4 | [file: string]: Array; 5 | } 6 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const tooltips = { 2 | 'TODO': `Something to be done`, 3 | 'FIXME': `Should be corrected.`, 4 | 'HACK': `A workaround.`, 5 | 'BUG': `A known bug that should be corrected.`, 6 | 'UNDONE': `A reversal or "roll back" of previous code.` 7 | }; 8 | -------------------------------------------------------------------------------- /src/functions/register-command.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function registerCommand(context: vscode.ExtensionContext, name: string, handler: (...args: any[]) => any) { 4 | context.subscriptions.push(vscode.commands.registerCommand(name, handler)); 5 | } 6 | -------------------------------------------------------------------------------- /icons/dark/collapse-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/light/collapse-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/dark/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/light/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/base.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config'; 2 | 3 | export abstract class BaseModule { 4 | 5 | constructor(protected config: Config) { } 6 | 7 | public updateConfiguration(config: Config) { 8 | this.config = config; 9 | this.onConfigChange(); 10 | } 11 | 12 | protected onConfigChange(): void { }; 13 | } -------------------------------------------------------------------------------- /src/functions/open-resource.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function openResource(resource: vscode.Uri, position: number): Promise { 4 | const editor = await vscode.window.showTextDocument(resource); 5 | const pos = editor.document.positionAt(position); 6 | editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter); 7 | editor.selection = new vscode.Selection(pos, pos); 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "rootDir": ".", 12 | "emitDecoratorMetadata": true, 13 | "noEmitHelpers": false, 14 | "experimentalDecorators": true 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | ".vscode-test" 19 | ] 20 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | expression: RegExp; 3 | exclude: string; 4 | include: string; 5 | scanOnSave: boolean; 6 | name?: string; 7 | trello: TrelloConfig; 8 | github: GitHubConfig; 9 | scheme: 'vscode' | 'vscode-insiders'; 10 | enableCommentFormatting: boolean; 11 | enableTelemetry: boolean; 12 | actionTypes: Array; 13 | enabledCodeActions: boolean; 14 | } 15 | 16 | export interface TrelloConfig { 17 | token?: string; 18 | defaultList?: string; 19 | } 20 | 21 | export interface GitHubConfig { 22 | auth?: string; 23 | storeCredentials?: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /icons/dark/todo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/functions/generate-comment.ts: -------------------------------------------------------------------------------- 1 | import { ActionComment } from '../models/action-comment'; 2 | 3 | export function generateComment(item: ActionComment, documentType = ''): string { 4 | switch (documentType.toLowerCase()) { 5 | case 'html': 6 | return ``; 7 | case 'css': 8 | return `/* ${item.commentType.toUpperCase()}${!!item.createdBy ? '(' + item.createdBy + ')' : ''}: ${item.label} */`; 9 | default: 10 | return `// ${item.commentType.toUpperCase()}${!!item.createdBy ? '(' + item.createdBy + ')' : ''}: ${item.label}`; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/models/action-comment.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | // export interface ActionComment { 4 | // text: string; 5 | // position: number; 6 | // commentType: string; 7 | // uri: vscode.Uri; 8 | // } 9 | 10 | export class ActionComment extends vscode.TreeItem { 11 | contextValue = ''; 12 | type: 'Action' | 'Value'; 13 | length: number; 14 | commentType: string; 15 | position: number; 16 | uri: vscode.Uri; 17 | createdBy?: string; 18 | 19 | constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState, contextValue: string = '') { 20 | super(label, collapsibleState); 21 | this.contextValue = contextValue; 22 | } 23 | } -------------------------------------------------------------------------------- /icons/light/todo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/functions/edit-comment.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ActionComment } from '../models/action-comment'; 3 | 4 | export async function editComment(item: ActionComment, value: string) { 5 | const editor = await vscode.window.showTextDocument(item.uri); 6 | let startPosition = editor.document.positionAt(item.position); 7 | await editor.edit(eb => { 8 | const endPosition = startPosition.with(startPosition.line, item.position + item.length); 9 | eb.replace(new vscode.Range(startPosition, endPosition), value); 10 | editor.selection = new vscode.Selection(startPosition, startPosition); 11 | editor.revealRange(new vscode.Range(startPosition, startPosition), vscode.TextEditorRevealType.InCenter); 12 | }); 13 | 14 | await editor.document.save(); 15 | } 16 | -------------------------------------------------------------------------------- /icons/light/fixme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/dark/fixme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/dark/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/light/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/uri-handler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class TodoUriHandler implements vscode.UriHandler { 4 | handleUri(uri: vscode.Uri): vscode.ProviderResult { 5 | switch (uri.path.toLowerCase()) { 6 | case '/view': 7 | this.handleView(uri); 8 | break; 9 | default: 10 | return; 11 | } 12 | } 13 | 14 | private async handleView(uri: vscode.Uri) { 15 | try { 16 | const filename = decodeURIComponent(uri.query.substring(5)); 17 | const position = +uri.fragment; 18 | const editor = await vscode.window.showTextDocument(vscode.Uri.parse('file:///' + filename)); 19 | const startPosition = editor.document.positionAt(position); 20 | editor.selection = new vscode.Selection(startPosition, startPosition); 21 | editor.revealRange(new vscode.Range(startPosition, startPosition)); 22 | } catch (e) { 23 | console.error(e); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /icons/dark/hack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /icons/light/hack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/functions/insert-comment.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ActionComment } from '../models/action-comment'; 3 | import { generateComment } from './generate-comment'; 4 | import { getDocumentType } from './get-document-type'; 5 | 6 | export async function insertComment(item: ActionComment) { 7 | const editor = await vscode.window.activeTextEditor; 8 | const startPosition = editor.selection.start; 9 | const beforeContent = editor.document.getText(new vscode.Range(startPosition.with(startPosition.line, 0), startPosition)); 10 | 11 | const docType = getDocumentType(editor.document.uri.fsPath); 12 | 13 | let value = generateComment(item, docType); 14 | if (beforeContent.trim() !== '') { 15 | // Add whitespace if inline 16 | value = ' ' + value; 17 | } 18 | 19 | await editor.edit(eb => { 20 | eb.insert(startPosition, value); 21 | editor.selection = new vscode.Selection(startPosition, startPosition); 22 | editor.revealRange(new vscode.Range(startPosition, startPosition), vscode.TextEditorRevealType.InCenter); 23 | }); 24 | 25 | await editor.document.save(); 26 | } 27 | -------------------------------------------------------------------------------- /src/functions/remove-comment.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function removeComment(resource: vscode.Uri, start: number, length: number) { 4 | const editor = await vscode.window.showTextDocument(resource); 5 | let startPosition = editor.document.positionAt(start); 6 | await editor.edit(eb => { 7 | const endPosition = startPosition.with(startPosition.line, start + length); 8 | 9 | // Check if line will become empty after remove 10 | const beforeContent = editor.document.getText(new vscode.Range(startPosition.with(startPosition.line, 0), startPosition)); 11 | if (beforeContent.trim() === '') { 12 | // Update start position to remove the line completely 13 | startPosition = editor.document.positionAt(start - beforeContent.length - 1); 14 | } else { 15 | // Remove trailing whitespaces 16 | const whitespaces = beforeContent.length - beforeContent.trimRight().length; 17 | startPosition = editor.document.positionAt(start - whitespaces); 18 | } 19 | eb.delete(new vscode.Range(startPosition, endPosition)); 20 | editor.selection = new vscode.Selection(startPosition, startPosition); 21 | editor.revealRange(new vscode.Range(startPosition, startPosition), vscode.TextEditorRevealType.InCenter); 22 | }); 23 | 24 | await editor.document.save(); 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/gmail.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { ActionComment } from '../models/action-comment'; 4 | import { Config } from '../config'; 5 | import { TrackFeature } from './telemetry'; 6 | import { registerCommand } from '../functions/register-command'; 7 | import { getSnippetPlainText } from '../functions/utils'; 8 | 9 | export class Gmail { 10 | 11 | constructor(context: vscode.ExtensionContext, private config: Config) { 12 | registerCommand(context, 'extension.sendUsingGmail', this.sendUsingGmail.bind(this)); 13 | } 14 | 15 | @TrackFeature('Send') 16 | private async sendUsingGmail(comment: ActionComment) { 17 | const body = await getSnippetPlainText(comment.uri, comment.position, 5); 18 | const content = encodeURI(body.replace(/ /g, '+')); 19 | const url = `https://mail.google.com/mail/?view=cm&fs=1&su=${comment.label}&body=${content}`; 20 | return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); 21 | } 22 | 23 | // private async getSnippet(resource: vscode.Uri, startAt: number, numberOfLines: number) { 24 | // const editor = await vscode.window.showTextDocument(resource); 25 | // const pos = editor.document.positionAt(startAt); 26 | // const startLine = Math.max(pos.line - 2, 0); 27 | // const endLine = Math.min(editor.document.lineCount, pos.line + numberOfLines); 28 | // let content = editor.document.getText(new vscode.Range(startLine, 0, endLine, 0)); 29 | // content = content.trim(); 30 | // const file = `${resource.fsPath}:${pos.line + 1}:${(pos.character || 0) + 1}`; 31 | // const line = '-'.repeat(file.length); 32 | // content = `Snippet:\n${line}\n${content}\n${line}\n${file}`; 33 | // content = encodeURI(content.replace(/ /g, '+')); 34 | // return content; 35 | // } 36 | } 37 | -------------------------------------------------------------------------------- /src/functions/register-tree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ActionCommentTreeViewProvider } from '../tree-provider'; 3 | import { Config } from '../config'; 4 | import { openResource } from './open-resource'; 5 | import { ActionComment } from '../models/action-comment'; 6 | import { TrackFeature } from '../modules/telemetry'; 7 | import { registerCommand } from './register-command'; 8 | 9 | export function registerTreeViewProvider(context: vscode.ExtensionContext, config: Config) { 10 | const actionCommentTreeViewProvider = new ActionCommentTreeViewProvider(config); 11 | const treeActions = new TreeActions(actionCommentTreeViewProvider); 12 | 13 | registerCommand(context, 'extension.openFile', treeActions.openFile.bind(this)); 14 | registerCommand(context, 'extension.viewComment', treeActions.viewComment.bind(this)); 15 | registerCommand(context, 'extension.refreshActionComments', treeActions.refreshActionComments.bind(treeActions)); 16 | registerCommand(context, 'extension.removeActionComment', treeActions.removeActionComment.bind(treeActions)); 17 | registerCommand(context, 'extension.collapseAll', treeActions.collapseAll.bind(treeActions)); 18 | 19 | context.subscriptions.push(vscode.window.registerTreeDataProvider('actionComments', actionCommentTreeViewProvider)); 20 | 21 | return actionCommentTreeViewProvider; 22 | } 23 | 24 | class TreeActions { 25 | 26 | constructor(private provider: ActionCommentTreeViewProvider) { } 27 | 28 | @TrackFeature('OpenFile') 29 | openFile(uri, position) { 30 | return openResource(uri, position); 31 | } 32 | 33 | @TrackFeature('ViewComment') 34 | viewComment(item: ActionComment) { 35 | return openResource(item.uri, item.position); 36 | } 37 | 38 | @TrackFeature('Refresh') 39 | refreshActionComments() { 40 | return this.provider.refresh(true); 41 | } 42 | 43 | @TrackFeature('Remove') 44 | removeActionComment(item: ActionComment) { 45 | return this.provider.removeItem(item.uri, item.position, item.length); 46 | } 47 | 48 | @TrackFeature('Collapse') 49 | collapseAll() { 50 | this.provider.collapseAll(); 51 | } 52 | } -------------------------------------------------------------------------------- /src/functions/create-tree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { existsSync } from 'fs'; 4 | 5 | import { ActionCommentCollection } from '../models/action-comment-collection'; 6 | import { ActionComment } from '../models/action-comment'; 7 | import { tooltips } from '../consts'; 8 | 9 | export function createTree(comments: ActionCommentCollection) { 10 | const actions: ActionCommentCollection = {}; 11 | 12 | Object.keys(comments) 13 | .map(filename => comments[filename].forEach(actionComment => { 14 | if (!actions[actionComment.commentType]) { 15 | actions[actionComment.commentType] = []; 16 | } 17 | actionComment.uri = actionComment.uri; 18 | actionComment.command = new OpenFileCommand(actionComment.uri, actionComment.position); 19 | actionComment.type = 'Value'; 20 | actionComment.contextValue = '$Comment'; 21 | actionComment.iconPath = { 22 | light: getIconPath(actionComment.commentType, 'light'), 23 | dark: getIconPath(actionComment.commentType, 'dark') 24 | }; 25 | actions[actionComment.commentType].push(actionComment); 26 | })); 27 | const topLevel = Object.keys(actions) 28 | .map(action => { 29 | const topLevelAction = new ActionComment(action, vscode.TreeItemCollapsibleState.Expanded, '$GROUP'); 30 | topLevelAction.tooltip = tooltips[action.toUpperCase()] || null; 31 | return topLevelAction; 32 | } ); 33 | return { items: topLevel, actions }; 34 | } 35 | 36 | class OpenFileCommand implements vscode.Command { 37 | command = 'extension.openFile'; 38 | title = 'Open File'; 39 | arguments?: any[]; 40 | 41 | constructor(uri: vscode.Uri, position: number) { 42 | this.arguments = [uri, position]; 43 | } 44 | } 45 | 46 | function getIconPath(type: string, theme: 'light' | 'dark') { 47 | const iconPath = path.join(__filename, '..', '..', '..', '..', 'icons', theme, type.toLowerCase() + '.svg'); 48 | if (existsSync(iconPath)) { 49 | return iconPath; 50 | } 51 | 52 | return path.join(__filename, '..', '..', '..', '..', 'icons', theme, 'todo.svg'); 53 | } -------------------------------------------------------------------------------- /src/modules/decorator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Config } from '../config'; 3 | 4 | export class Deocrator { 5 | 6 | private listeners: Array = []; 7 | private commentTypeStyle = vscode.window.createTextEditorDecorationType({ color: '#ffaa00' }); 8 | private commentContentStyle = vscode.window.createTextEditorDecorationType({ fontStyle: 'italic' }); 9 | 10 | constructor(context: vscode.ExtensionContext, private config: Config) { 11 | if (config.enableCommentFormatting) { 12 | this.registerListeners(); 13 | } 14 | } 15 | 16 | updateConfiguration(config: Config) { 17 | this.config = config; 18 | if (config.enableCommentFormatting && this.listeners.length === 0) { 19 | this.registerListeners(); 20 | } 21 | if (!config.enableCommentFormatting) { 22 | this.unregisterListeners(); 23 | } 24 | } 25 | 26 | private registerListeners() { 27 | let activeEditor = vscode.window.activeTextEditor; 28 | if (activeEditor) { 29 | this.updateDecorations(vscode.window.activeTextEditor, this.config); 30 | } 31 | 32 | this.listeners.push(vscode.window.onDidChangeActiveTextEditor(editor => { 33 | activeEditor = editor; 34 | if (editor) { 35 | this.updateDecorations(vscode.window.activeTextEditor, this.config); 36 | } 37 | })); 38 | 39 | this.listeners.push(vscode.workspace.onDidChangeTextDocument(event => { 40 | if (activeEditor && event.document === activeEditor.document) { 41 | this.updateDecorations(vscode.window.activeTextEditor, this.config); 42 | } 43 | })); 44 | } 45 | 46 | private unregisterListeners() { 47 | this.listeners.forEach(p => p.dispose()); 48 | this.listeners = []; 49 | } 50 | 51 | private updateDecorations(activeEditor: vscode.TextEditor, config: Config) { 52 | if (!activeEditor) { 53 | return; 54 | } 55 | const regEx = config.expression; 56 | const text = activeEditor.document.getText(); 57 | const commentTypes: vscode.DecorationOptions[] = []; 58 | const contents: vscode.DecorationOptions[] = []; 59 | let match: RegExpExecArray; 60 | while (match = regEx.exec(text)) { 61 | const startPos = activeEditor.document.positionAt(match.index + match[0].indexOf(match[1])); 62 | const endPos = startPos.translate(0, match[1].length); 63 | const decoration = { range: new vscode.Range(startPos, endPos) }; 64 | commentTypes.push(decoration); 65 | 66 | const content = match[match.length - 1]; 67 | const contentStartPos = activeEditor.document.positionAt(match.index + match[0].indexOf(content)); 68 | const contentEndPos = contentStartPos.translate(0, content.length); 69 | contents.push({ range: new vscode.Range(contentStartPos, contentEndPos) }); 70 | } 71 | 72 | activeEditor.setDecorations(this.commentTypeStyle, commentTypes); 73 | activeEditor.setDecorations(this.commentContentStyle, contents); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/modules/code-actions.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Config } from '../config'; 4 | 5 | import { registerCommand } from '../functions/register-command'; 6 | import { parseComment } from '../functions/read-comments'; 7 | 8 | export class CodeActions { 9 | 10 | private providers: Array = []; 11 | 12 | constructor(private context: vscode.ExtensionContext, private config: Config) { 13 | registerCommand(context, 'extension.contextMenu', this.contextMenuHandler.bind(this)); 14 | 15 | if (config.enabledCodeActions) { 16 | this.setupCodeActions(); 17 | } 18 | } 19 | 20 | updateConfiguration(config: Config) { 21 | this.config = config; 22 | if (config.enabledCodeActions) { 23 | this.setupCodeActions(); 24 | } else { 25 | this.disableCodeActions(); 26 | } 27 | } 28 | 29 | private setupCodeActions() { 30 | if (this.providers.length > 0) { 31 | return; 32 | } 33 | const fixProvider = this.createCodeActionProvider(); 34 | this.providers.push(vscode.languages.registerCodeActionsProvider({ scheme: 'file', language: 'typescript' }, fixProvider)); 35 | this.providers.push(vscode.languages.registerCodeActionsProvider({ scheme: 'file', language: 'javascript' }, fixProvider)); 36 | this.context.subscriptions.push(...this.providers); 37 | } 38 | 39 | private disableCodeActions() { 40 | this.providers.forEach(p => p.dispose()); 41 | this.providers = []; 42 | } 43 | 44 | private createCodeActionProvider() { 45 | const fixProvider: vscode.CodeActionProvider = { 46 | provideCodeActions: function (document, range, context, token) { 47 | const codeActionCreator = new CodeActionCreator(document, range, context); 48 | return [ 49 | codeActionCreator.create('Create Trello Card', 'extension.createTrelloCard', vscode.CodeActionKind.QuickFix), 50 | codeActionCreator.create('Send using Gmail', 'extension.sendUsingGmail', vscode.CodeActionKind.QuickFix) 51 | ]; 52 | } 53 | }; 54 | 55 | return fixProvider; 56 | } 57 | 58 | private contextMenuHandler(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, commandName: string) { 59 | try { 60 | const start = new vscode.Position(range.start.line, 0); 61 | const end = start.translate(1, 0); 62 | 63 | const row = document.getText(range.with(start, end)).trim(); 64 | const c = parseComment(this.config.expression, row, document.uri, document.offsetAt(start)); 65 | vscode.commands.executeCommand(commandName, c); 66 | } catch (e) { 67 | console.error('CodeAction Error', e); 68 | } 69 | } 70 | } 71 | 72 | class CodeActionCreator { 73 | constructor( 74 | private document: vscode.TextDocument, 75 | private range: vscode.Range | vscode.Selection, 76 | private context: vscode.CodeActionContext 77 | ) { } 78 | 79 | create(title: string, command: string, kind: vscode.CodeActionKind) { 80 | return { 81 | title, 82 | kind, 83 | command: 'extension.contextMenu', 84 | arguments: [this.document, this.range, this.context, command] 85 | }; 86 | } 87 | } -------------------------------------------------------------------------------- /src/tree-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Config } from './config'; 4 | import { ActionComment } from './models/action-comment'; 5 | import { ActionCommentCollection } from './models/action-comment-collection'; 6 | import { readComments, readCommentsInFile } from './functions/read-comments'; 7 | import { removeComment } from './functions/remove-comment'; 8 | import { createTree } from './functions/create-tree'; 9 | 10 | export class ActionCommentTreeViewProvider implements vscode.TreeDataProvider { 11 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 12 | private tree: ActionCommentCollection; 13 | private comments: ActionCommentCollection; 14 | 15 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 16 | 17 | constructor(private config: Config) { 18 | vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { 19 | if (config.scanOnSave) { 20 | this.refresh(true, document.uri); 21 | } 22 | }); 23 | 24 | this.refresh(true); 25 | } 26 | 27 | async updateConfiguration(config: Config) { 28 | this.config = config; 29 | try { 30 | await this.refresh(true); 31 | } catch (e) { 32 | console.error('Could not refresh tree after config change', e); 33 | } 34 | } 35 | 36 | getTreeItem(element: ActionComment): vscode.TreeItem | Thenable { 37 | return element; 38 | } 39 | 40 | getChildren(element?: ActionComment): vscode.ProviderResult { 41 | if (this.tree && element) { 42 | return Promise.resolve(this.tree[element.label]); 43 | } 44 | 45 | if (!!this.comments) { 46 | const tree = createTree(this.comments); 47 | return tree.items; 48 | } 49 | 50 | return []; 51 | } 52 | 53 | getParent?(element: ActionComment): vscode.ProviderResult { 54 | return null; 55 | } 56 | 57 | async refresh(emitChange?: boolean, file?: vscode.Uri) { 58 | try { 59 | if (file) { 60 | const fileComments = await readCommentsInFile(this.config.expression, file); 61 | const key = vscode.workspace.asRelativePath(file, true); 62 | if (!!fileComments) { 63 | this.comments[key] = fileComments; 64 | } else { 65 | if (key in this.comments) { 66 | delete this.comments[key]; 67 | } 68 | } 69 | } else { 70 | this.comments = await readComments(this.config); 71 | } 72 | 73 | const tree = createTree(this.comments); 74 | this.tree = tree.actions; 75 | if (emitChange) { 76 | this._onDidChangeTreeData.fire(); 77 | } 78 | return Promise.resolve(tree.items); 79 | } catch (e) { 80 | return Promise.reject(e); 81 | } 82 | } 83 | 84 | async removeItem(resource: vscode.Uri, start: number, length: number) { 85 | await removeComment(resource, start, length); 86 | this.refresh(true, resource); 87 | } 88 | 89 | collapseAll() { 90 | // TODO: implement 91 | this.refresh(false); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/functions/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as https from 'https'; 3 | import * as http from 'http'; 4 | 5 | export interface SnippetResult { 6 | snippet: string; 7 | filename: string; 8 | } 9 | 10 | export async function getSnippet(resource: vscode.Uri, startAt: number, numberOfLines: number): Promise { 11 | const editor = await vscode.window.showTextDocument(resource); 12 | const pos = editor.document.positionAt(startAt); 13 | const startLine = Math.max(pos.line - 2, 0); 14 | const endLine = Math.min(editor.document.lineCount, pos.line + numberOfLines); 15 | let content = editor.document.getText(new vscode.Range(startLine, 0, endLine, 0)); 16 | content = content.trim(); 17 | const file = `${resource.fsPath}:${pos.line + 1}:${(pos.character || 0) + 1}`; 18 | 19 | return { 20 | snippet: content, 21 | filename: file 22 | }; 23 | } 24 | 25 | export async function getSnippetMarkdown(resource: vscode.Uri, startAt: number, numberOfLines: number) { 26 | const snippet = await getSnippet(resource, startAt, numberOfLines); 27 | const lang = vscode.window.activeTextEditor.document.languageId; 28 | return `\`\`\`${lang}\n${snippet.snippet}\n\`\`\`\n---\n${snippet.filename}`; 29 | } 30 | 31 | export async function getSnippetPlainText(resource: vscode.Uri, startAt: number, numberOfLines: number) { 32 | const snippet = await getSnippet(resource, startAt, numberOfLines); 33 | 34 | const line = '-'.repeat(snippet.filename.length); 35 | return `${line}\n${snippet.snippet}\n${line}\n${snippet.filename}`; 36 | } 37 | 38 | export function httpGet(url: string): Promise { 39 | return new Promise((resolve, reject) => { 40 | https.get(url, (res: http.IncomingMessage) => { 41 | res.setEncoding('utf8'); 42 | let rawData = ''; 43 | res.on('data', (chunk) => { rawData += chunk; }); 44 | res.on('end', () => { 45 | try { 46 | const parsedData = JSON.parse(rawData); 47 | resolve(parsedData); 48 | } catch (e) { 49 | reject(e); 50 | } 51 | }); 52 | }).on('error', (e) => reject(e)); 53 | }); 54 | } 55 | 56 | export function httpPost(hostname: string, urlPath: string, data: any, headers?: any) { 57 | return new Promise((resolve, reject) => { 58 | data = JSON.stringify(data); 59 | const options: http.RequestOptions = { 60 | hostname: hostname, 61 | port: 443, 62 | path: urlPath, 63 | method: 'POST', 64 | headers: { 65 | 'User-Agent': 'VSCode TODO List Extension', 66 | 'Content-Type': 'application/json', 67 | 'Content-Length': Buffer.byteLength(data), 68 | ...headers 69 | } 70 | }; 71 | 72 | const req = https.request(options, (res) => { 73 | res.setEncoding('utf8'); 74 | let rData = ''; 75 | res.on('data', (chunk) => { 76 | rData += chunk; 77 | }); 78 | res.on('end', () => { 79 | resolve(rData); 80 | }); 81 | }); 82 | 83 | req.on('error', (e) => { 84 | reject(e); 85 | }); 86 | 87 | // write data to request body 88 | req.write(data); 89 | req.end(); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/telemetry.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Config } from '../config'; 3 | 4 | const APP_INSIGHTS_KEY = '4bf4cf26-e6f8-4d6d-bb6f-9e7b3cee7cdf'; 5 | 6 | export class Telemetry { 7 | 8 | private static appInsights: any; 9 | private static client: any; 10 | private static initialized = false; 11 | private static enabled: boolean; 12 | 13 | static init(config: Config) { 14 | this.enabled = vscode.debug.activeDebugSession === undefined && config.enableTelemetry; 15 | 16 | if (this.initialized || !this.enabled) { 17 | return; 18 | } 19 | 20 | try { 21 | this.appInsights = require('applicationinsights'); 22 | this.appInsights.setup(APP_INSIGHTS_KEY) 23 | .setAutoDependencyCorrelation(false) 24 | .setAutoCollectRequests(false) 25 | .setAutoCollectPerformance(false) 26 | .setAutoCollectExceptions(true) 27 | .setAutoCollectDependencies(false) 28 | .setAutoCollectConsole(false) 29 | .setUseDiskRetryCaching(true) 30 | .setSendLiveMetrics(true) 31 | .start(); 32 | this.client = this.appInsights.defaultClient; 33 | 34 | const extension = vscode.extensions.getExtension('TzachOvadia.todo-list'); 35 | this.appInsights.defaultClient.context.tags[this.appInsights.defaultClient.context.keys.cloudRole] = extension.packageJSON.name; 36 | 37 | if (vscode && vscode.env) { 38 | this.client.context.tags[this.client.context.keys.userId] = vscode.env.machineId; 39 | this.client.context.tags[this.client.context.keys.sessionId] = vscode.env.sessionId; 40 | } 41 | 42 | this.initialized = true; 43 | } catch (e) { 44 | console.error('Could not initialize Telemetry', e); 45 | } 46 | } 47 | 48 | static updateConfiguration(config: Config) { 49 | this.init(config); 50 | } 51 | 52 | static trackLoad() { 53 | if (!this.enabled) { 54 | return; 55 | } 56 | this.client.context.tags[this.client.context.keys.operationName] = 'state'; 57 | this.client.trackTrace({ message: 'Load' }); 58 | } 59 | 60 | static trackException(error: Error, initiator?: string) { 61 | if (!this.enabled) { 62 | return; 63 | } 64 | this.client.context.tags[this.client.context.keys.operationName] = 'state'; 65 | const event = { exception: error, properties: {} }; 66 | if (initiator) { 67 | event.properties = { initiator }; 68 | } 69 | this.client.trackException(event); 70 | } 71 | 72 | static trackFeatureActivation(operationName: string, featureName: string) { 73 | if (!this.enabled) { 74 | return; 75 | } 76 | 77 | this.client.context.tags[this.client.context.keys.operationParentId] = 'featureActivated'; 78 | this.client.context.tags[this.client.context.keys.operationName] = operationName; 79 | const event = { 80 | name: featureName, 81 | }; 82 | this.client.trackEvent(event); 83 | } 84 | } 85 | 86 | export function TrackFeature(name?: string) { 87 | return function (target, propertyKey: string, descriptor: PropertyDescriptor) { 88 | var originalMethod = descriptor.value; 89 | 90 | descriptor.value = function (...args: any[]) { 91 | try { 92 | Telemetry.trackFeatureActivation(target.constructor.name, name || propertyKey); 93 | return originalMethod.apply(this, args); 94 | } catch (e) { 95 | Telemetry.trackException(e); 96 | throw e; 97 | } 98 | } 99 | 100 | return descriptor; 101 | } 102 | } -------------------------------------------------------------------------------- /src/modules/modifications.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | const micromatch = require('micromatch'); 3 | const clipboardy = require('clipboardy'); 4 | 5 | import { Config } from '../config'; 6 | import { ActionComment } from '../models/action-comment'; 7 | import { generateComment } from '../functions/generate-comment'; 8 | import { editComment } from '../functions/edit-comment'; 9 | import { insertComment } from '../functions/insert-comment'; 10 | import { TrackFeature } from './telemetry'; 11 | import { registerCommand } from '../functions/register-command'; 12 | import { tooltips } from '../consts'; 13 | import { getDocumentType } from '../functions/get-document-type'; 14 | 15 | export class Modifications { 16 | 17 | constructor(context: vscode.ExtensionContext, private config: Config) { 18 | registerCommand(context, 'extension.editComment', this.editCommentCommand.bind(this)); 19 | registerCommand(context, 'extension.insertComment', this.insertCommentCommand.bind(this)); 20 | registerCommand(context, 'extension.copyComment', this.copyCommentCommand.bind(this)); 21 | } 22 | 23 | updateConfiguration(config: Config) { 24 | this.config = config; 25 | } 26 | 27 | @TrackFeature('Edit') 28 | private async editCommentCommand(item: ActionComment) { 29 | item = await this.getUserInputs(item); 30 | 31 | if (!item) { 32 | return; 33 | } 34 | 35 | const docType = getDocumentType(item.uri.fsPath); 36 | 37 | const newComment = generateComment(item, docType); 38 | editComment(item, newComment); 39 | } 40 | 41 | @TrackFeature('Insert') 42 | private async insertCommentCommand() { 43 | const activeEditor = vscode.window.activeTextEditor; 44 | if (!activeEditor || !activeEditor.document) { 45 | return; 46 | } 47 | 48 | const extensionSupported = micromatch.isMatch(activeEditor.document.uri.fsPath, this.config.include); 49 | if (!extensionSupported) { 50 | return; 51 | } 52 | 53 | const item = await this.getUserInputs(); 54 | if (!item) { 55 | return; 56 | } 57 | insertComment(item); 58 | } 59 | 60 | @TrackFeature('Copy') 61 | private async copyCommentCommand(item: ActionComment) { 62 | const docType = getDocumentType(item.uri.fsPath); 63 | const text = generateComment(item, docType); 64 | await clipboardy.write(text); 65 | } 66 | 67 | private async getUserInputs(item?: ActionComment) { 68 | item = item || new ActionComment(null); 69 | const data = [ 70 | { prompt: 'Comment type', value: item && item.commentType, key: 'commentType', options: this.config.actionTypes }, 71 | { prompt: 'Text', value: item && item.label, key: 'label' }, 72 | { prompt: 'Created by', value: (item && item.createdBy) || this.config.name, key: 'createdBy' } 73 | ]; 74 | 75 | for (let i = 0; i < data.length; i++) { 76 | const input = data[i]; 77 | let newValue: string; 78 | if (input.options) { 79 | const options: Array = input.options.map(o => { 80 | const label = o; 81 | const description = tooltips[o] || null; 82 | return { label, description }; 83 | }); 84 | const userSelection = await vscode.window.showQuickPick(options, { placeHolder: input.prompt }); 85 | newValue = userSelection && userSelection.label; 86 | } else { 87 | newValue = await vscode.window.showInputBox({ prompt: input.prompt, value: input.value }); 88 | } 89 | 90 | if (newValue === undefined) { 91 | return; 92 | } 93 | 94 | item[input.key] = newValue; 95 | } 96 | 97 | return item; 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /src/modules/github.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | const parse = require('parse-git-config'); 3 | 4 | import { Config } from '../config'; 5 | import { ActionComment } from '../models/action-comment'; 6 | import { TrackFeature } from './telemetry'; 7 | import { registerCommand } from '../functions/register-command'; 8 | import { BaseModule } from './base'; 9 | import { httpPost, getSnippetMarkdown } from '../functions/utils'; 10 | 11 | export class GitHub extends BaseModule { 12 | 13 | constructor(context: vscode.ExtensionContext, config: Config) { 14 | super(config); 15 | 16 | registerCommand(context, 'extension.createGitHubIssue', this.createGitHubIssue.bind(this)); 17 | } 18 | 19 | protected onConfigChange() { 20 | if (this.config.github && !this.config.github.storeCredentials && !!this.config.github.auth) { 21 | vscode.workspace.getConfiguration('github').update('auth', null, vscode.ConfigurationTarget.Global); 22 | } 23 | } 24 | 25 | @TrackFeature('Create GitHub Issue') 26 | private async createGitHubIssue(item: ActionComment) { 27 | const credentials = await this.getAuth(); 28 | if (!credentials) { 29 | return; 30 | } 31 | 32 | const commentSnippet = await getSnippetMarkdown(item.uri, item.position, 5); 33 | 34 | const title = `${item.commentType}: ${item.label}`; 35 | const body = commentSnippet; 36 | 37 | const repoPath = this.getRepositoryPath(); 38 | const response = await httpPost(`api.github.com`, `/repos/${repoPath}/issues`, { title, body }, { 39 | 'Authorization': `Basic ${credentials}` 40 | }); 41 | if (response) { 42 | try { 43 | const result = JSON.parse(response); 44 | if (result.html_url) { 45 | const userAction = await vscode.window.showInformationMessage(`GitHub issue created!\nURL: ${result.html_url}`, 'Open'); 46 | if (userAction === 'Open') { 47 | return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(result.html_url)); 48 | } 49 | } 50 | console.log(result); 51 | } catch (e) { 52 | vscode.window.showErrorMessage(`There was a problem creating GitHub issue\n${e.message}`); 53 | } 54 | } 55 | } 56 | 57 | private getRepositoryPath() { 58 | const cwd = vscode.workspace.workspaceFolders[0].uri.fsPath; 59 | const gitConfig = parse.sync({ cwd, path: '.git/config' }); 60 | const keys = parse.expandKeys(gitConfig); 61 | const noProto = keys.remote.origin.url.substr(keys.remote.origin.url.indexOf('//') + 2); 62 | return noProto.substr(noProto.indexOf('/') + 1).replace('.git', ''); 63 | } 64 | 65 | private async getAuth() { 66 | const Cryptr = require('cryptr'); 67 | const sk = `${vscode.env.appName}:${vscode.env.machineId}`; 68 | const cryptr = new Cryptr(sk); 69 | 70 | let encryptedCredentials = this.config.github && this.config.github.auth && this.config.github.storeCredentials ? this.config.github.auth : null; 71 | if (!!encryptedCredentials) { 72 | console.log(encryptedCredentials); 73 | return cryptr.decrypt(encryptedCredentials); 74 | } 75 | 76 | const username = await vscode.window.showInputBox({ prompt: 'GitHub Username' }); 77 | if (!username) { 78 | return; 79 | } 80 | 81 | const password = await vscode.window.showInputBox({ prompt: 'GitHub Password', password: true }); 82 | if (!password) { 83 | return; 84 | } 85 | 86 | const credentials = Buffer.from(`${username}:${password}`).toString('base64'); 87 | 88 | if (this.config.github && this.config.github.storeCredentials) { 89 | const encryptedString = cryptr.encrypt(credentials); 90 | vscode.workspace.getConfiguration('github').update('auth', encryptedString, vscode.ConfigurationTarget.Global); 91 | } 92 | return credentials; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # TODO List 2 | 3 | List all of your TODO-type comments in an easy-to-read tree view panel. 4 | `Syntax Highlighting`, `Trello`/`Gmail`/`GitHub` `Integration` included. 5 | 6 | 7 | https://marketplace.visualstudio.com/items?itemName=TzachOvadia.todo-list 8 | 9 | ![Usage Example](images/insert-command.gif) 10 | 11 | ## Table of Contents 12 | - [Features](#Features) 13 | - [Supported Comments](#Supported-Comments) 14 | - [Settings](#Settings) 15 | - [Supported Languages](#Supported-Languages) 16 | 17 | ## Features 18 | ### Convenient way to view tagged comments 19 | 20 | ![Preview](images/preview.png) 21 | 22 | ### Remove, Copy & Edit comments easily 23 | 24 | ![Context Menu](images/context.png) 25 | 26 | #### Usage 27 | `Right-Click` a comment in `Action Comments` -> Remove/Copy/Edit 28 | 29 | ### Insert comment quickly 30 | Press `Ctrl`/`CMD` + `Shift` + `A` to insert a comment in current cursor position. 31 | 32 | ### Comment formatting 33 | 34 | ![Comment Format](images/highlight.png) 35 | 36 | ### **Trello Integration** - Create Trello card directly from your IDE 37 | 38 | ![Trello Card](images/trello.gif) 39 | 40 | #### Usage 41 | - Right-Click a comment in `Action Comments` list -> `Create Trello Card`. 42 | > On first use, you'll have to supply a token. If you don't have a token, just press `Esc` and follow the notification instructions. 43 | > 44 | > Next, you'll need to select a Trello list to create cards in. 45 | - That's it. The card is created in the list you selected on first use. 46 | 47 | ### **Gmail Integration** - Send comment to mail recipient 48 | 49 | ![Gmail Integration](images/gmail.gif) 50 | 51 | #### Usage 52 | - Right-Click a comment in `Action Comments` list -> `Send using Gmail`. 53 | 54 | ### **GitHub Integration** - Open GitHub issue directly from your IDE 55 | 56 | #### Usage 57 | - Right-Click a comment in `Action Comments` list -> `Open GitHub Issue`. 58 | - Enter your GitHub username and password. 59 | 60 | ## Supported Comments 61 | TODO List supports any comment written in the next formats: 62 | ``` 63 | // (NAME*): 64 | /* : */ 65 | 66 | ``` 67 | 68 | *Name is optional 69 | 70 | Examples: 71 | ``` 72 | // TODO: Refactor everything 73 | /* FIXME: Please please please */ 74 | // HACK(john): This is a workaround 75 | 76 | ``` 77 | 78 | Common tags/types: 79 | - `TODO` – something to be done. 80 | - `FIXME` – should be corrected. 81 | - `HACK` – a workaround. 82 | - `BUG` – a known bug that should be corrected. 83 | - `UNDONE` – a reversal or "roll back" of previous code. 84 | 85 | ## Settings 86 | 87 | - **Expression** 88 | 89 | RegExp to use for extracting comments (first group must be type, last must be text). We recommend capturing only all-uppercase types to avoid capturing `tslint:` and commented properties. 90 | 91 | Default: ```(?:\/\/|\/\*|\<\!--)[ ]?([A-Z]+)(?:\:|\(([A-Za-z\/\d ]+)\)\:)[ ]?(.*?)[ ]?(?:--\>|\*\/|$)``` 92 | 93 | - **Scan On Save** 94 | 95 | Scan comments when saving a file. 96 | 97 | Default: ```true``` 98 | 99 | **Include** 100 | 101 | Glob pattern to include in scans. 102 | 103 | Default: ```**/*.{ts,js,php,css,scss,html}``` 104 | 105 | - **Exclude** 106 | 107 | Glob pattern to exclude from scans. 108 | 109 | Default: ```{**/node_modules/**,**/bower_components/**,**/dist/**,**/build/**,**/.vscode/**,**/_output/**,**/vendor/**,**/*.min.*,**/*.map}``` 110 | 111 | - **Name** 112 | 113 | Name to use as `Created by`. 114 | 115 | Default: `empty` 116 | 117 | **Action Types** 118 | 119 | Comma-separated action types to select from when inserting a new comment. 120 | 121 | Default: `TODO,FIXME,HACK,BUG,UNDONE` 122 | 123 | - **Enable Comment Formatting** 124 | 125 | Enable comment formatting (Set color for comment type and make text italic) 126 | 127 | Default: `true` 128 | 129 | - **Trello:Token** 130 | 131 | In order to create Trello cards, this extension requires read and write permissions. 132 | 133 | [Click here to generate token](https://trello.com/1/authorize?name=TODO%20List&scope=read,write&expiration=never&response_type=token&key=a20752c7ff035d5001ce2938f298be64). 134 | 135 | - **Trello:Default List** 136 | 137 | List ID to create cards in (will be automatically set on first use) 138 | 139 | ## Supported Languages 140 | This extension currently supports `Javascript`, `Typescript`, `PHP`, `CSS/SCSS` and `HTML`. -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { Config, TrelloConfig, GitHubConfig } from './config'; 5 | import { registerTreeViewProvider } from './functions/register-tree'; 6 | import { TodoUriHandler } from './modules/uri-handler'; 7 | import { Trello } from './modules/trello'; 8 | import { Modifications } from './modules/modifications'; 9 | import { Deocrator } from './modules/decorator'; 10 | import { Gmail } from './modules/gmail'; 11 | import { Telemetry } from './modules/telemetry'; 12 | import { GitHub } from './modules/github'; 13 | import { CodeActions } from './modules/code-actions'; 14 | 15 | export async function activate(context: vscode.ExtensionContext) { 16 | try { 17 | let config = getConfig(); 18 | 19 | const tree = registerTreeViewProvider(context, config); 20 | 21 | context.subscriptions.push(vscode.window.registerUriHandler(new TodoUriHandler())); 22 | 23 | await setupTelemetry(config); 24 | const trello = new Trello(context, config); 25 | const modifications = new Modifications(context, config); 26 | const decorator = new Deocrator(context, config); 27 | const gmail = new Gmail(context, config); 28 | const codeActions = new CodeActions(context, config); 29 | const gitHub = new GitHub(context, config); 30 | 31 | context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { 32 | config = getConfig(); 33 | if (e.affectsConfiguration('trello')) { 34 | trello.updateConfiguration(config); 35 | } 36 | if (e.affectsConfiguration('name')) { 37 | modifications.updateConfiguration(config); 38 | } 39 | if (e.affectsConfiguration('expression') || e.affectsConfiguration('enableCommentFormatting')) { 40 | decorator.updateConfiguration(config); 41 | } 42 | if (e.affectsConfiguration('enableTelemetry')) { 43 | Telemetry.updateConfiguration(config); 44 | } 45 | if (e.affectsConfiguration('expression') || e.affectsConfiguration('enabledCodeActions')) { 46 | codeActions.updateConfiguration(config); 47 | } 48 | if (e.affectsConfiguration('expression') || e.affectsConfiguration('include') || e.affectsConfiguration('exclude')) { 49 | tree.updateConfiguration(config); 50 | } 51 | if (e.affectsConfiguration('github')) { 52 | gitHub.updateConfiguration(config); 53 | } 54 | })); 55 | } catch (e) { 56 | vscode.window.showErrorMessage('Could not activate TODO List (' + e.message + ')'); 57 | } 58 | } 59 | 60 | export function deactivate() { } 61 | 62 | async function setupTelemetry(config: Config) { 63 | if (config.enableTelemetry === null) { 64 | const message = 'Enable minimal telemetry? This will let us know which features are more useful so we could make them better - No personal or project data will be sent'; 65 | const enableAction = 'Yes'; 66 | const cancelAction = 'No'; 67 | const userResponse = await vscode.window.showInformationMessage(message, enableAction, cancelAction); 68 | const enableTelemetry = userResponse === enableAction ? true : false; 69 | vscode.workspace.getConfiguration().update('enableTelemetry', enableTelemetry, vscode.ConfigurationTarget.Global); 70 | config.enableTelemetry = enableTelemetry; 71 | } 72 | Telemetry.init(config); 73 | Telemetry.trackLoad(); 74 | } 75 | 76 | function getConfig() { 77 | const appScheme = vscode.version.indexOf('insider') > -1 ? 'vscode-insiders' : 'vscode' 78 | const configuration = vscode.workspace.getConfiguration(); 79 | 80 | const config: Config = { 81 | expression: new RegExp(configuration.get('expression'), 'gm'), 82 | exclude: configuration.get('exclude'), 83 | include: configuration.get('include'), 84 | scanOnSave: configuration.get('scanOnSave'), 85 | name: configuration.get('name'), 86 | trello: configuration.get('trello'), 87 | scheme: appScheme, 88 | enableCommentFormatting: configuration.get('enableCommentFormatting'), 89 | enableTelemetry: configuration.get('enableTelemetry', null), 90 | actionTypes: configuration.get('actionTypes').toUpperCase().trim().split(','), 91 | github: configuration.get('github'), 92 | enabledCodeActions: configuration.get('enabledCodeActions') 93 | }; 94 | 95 | return config; 96 | } 97 | -------------------------------------------------------------------------------- /src/functions/read-comments.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { promises } from 'fs'; 3 | 4 | import { ActionCommentCollection } from '../models/action-comment-collection'; 5 | import { ActionComment } from '../models/action-comment'; 6 | import { Config } from '../config'; 7 | 8 | export async function readComments(config: Config): Promise { 9 | try { 10 | const files = await vscode.workspace.findFiles(config.include, config.exclude); 11 | const result: ActionCommentCollection = await createObject(config.expression, files); 12 | return Promise.resolve(result); 13 | } catch (err) { 14 | return Promise.reject(err); 15 | } 16 | } 17 | 18 | async function createObject(expression: RegExp, files: Array): Promise { 19 | const result: ActionCommentCollection = {}; 20 | for (const file of files) { 21 | const key = vscode.workspace.asRelativePath(file, true); 22 | const comments = await readCommentsInFile(expression, file); 23 | if (!!comments) { 24 | result[key] = comments; 25 | } 26 | } 27 | 28 | return result; 29 | } 30 | 31 | export async function readCommentsInFile(expression: RegExp, file: vscode.Uri) { 32 | let fileContent: string; 33 | try { 34 | fileContent = await promises.readFile(file.fsPath, 'utf8'); 35 | } catch (e) { 36 | console.warn(`readCommentsInFile() readFile failed (${file.fsPath})`, e); 37 | return null; 38 | } 39 | const hasBOM = /^\uFEFF/.test(fileContent); 40 | 41 | let res: RegExpExecArray; 42 | const currentFileActions: Array = []; 43 | while (res = expression.exec(fileContent)) { 44 | const groups = { 45 | type: res[1], 46 | name: res[2], 47 | text: res[res.length - 1] 48 | }; 49 | if (res.length < 4) { 50 | groups.name = null; 51 | } 52 | const label = groups.text.replace(/[ ]?\*\/$/, ''); 53 | const commentType = (groups.type || 'TODO').toUpperCase(); 54 | const comment: ActionComment = new ActionComment(label); 55 | const tooltip = []; 56 | if (groups.name) { 57 | tooltip.push(`Created by ${groups.name}`); 58 | comment.createdBy = groups.name; 59 | } 60 | tooltip.push(file.fsPath); 61 | 62 | let position = expression.lastIndex - res[0].length; 63 | if (hasBOM) { 64 | position--; 65 | } 66 | 67 | currentFileActions.push({ 68 | ...comment, 69 | commentType, 70 | position, 71 | uri: file, 72 | type: 'Action', 73 | contextValue: commentType, 74 | tooltip: tooltip.join('\n'), 75 | length: res[0].length, 76 | id: `${encodeURIComponent(file.path)}_${expression.lastIndex - res[0].length}` 77 | }); 78 | } 79 | if (currentFileActions.length > 0) { 80 | return currentFileActions.sort((a, b) => a.position > b.position ? 1 : ((b.position > a.position) ? -1 : 0)); 81 | } 82 | 83 | return null; 84 | } 85 | 86 | export function parseComment(expression: RegExp, text: string, file: vscode.Uri, position: any) { 87 | const hasBOM = /^\uFEFF/.test(text); 88 | const exp = expression.compile(); // HACK: reset expression 89 | const res = exp.exec(text); 90 | 91 | const groups = { 92 | type: res[1], 93 | name: res[2], 94 | text: res[res.length - 1] 95 | }; 96 | if (res.length < 4) { 97 | groups.name = null; 98 | } 99 | const label = groups.text.replace(/[ ]?\*\/$/, ''); 100 | const commentType = (groups.type || 'TODO').toUpperCase(); 101 | const comment: ActionComment = new ActionComment(label); 102 | const tooltip = []; 103 | if (groups.name) { 104 | tooltip.push(`Created by ${groups.name}`); 105 | comment.createdBy = groups.name; 106 | } 107 | tooltip.push(file.fsPath); 108 | 109 | // let position = expression.lastIndex - res[0].length; 110 | if (hasBOM) { 111 | position--; 112 | } 113 | 114 | const id = file ? encodeURIComponent(file.path) + '_' : '' + `${expression.lastIndex - res[0].length}`; 115 | const uri = file || undefined; 116 | return { 117 | ...comment, 118 | commentType, 119 | position, 120 | uri, 121 | type: 'Action', 122 | contextValue: commentType, 123 | tooltip: tooltip.join('\n'), 124 | length: res[0].length, 125 | id 126 | }; 127 | } -------------------------------------------------------------------------------- /src/modules/trello.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as https from 'https'; 3 | import * as http from 'http'; 4 | const clipboardy = require('clipboardy'); 5 | 6 | import { Config } from '../config'; 7 | import { ActionComment } from '../models/action-comment'; 8 | import { TrackFeature } from './telemetry'; 9 | import { registerCommand } from '../functions/register-command'; 10 | 11 | export class Trello { 12 | 13 | constructor(context: vscode.ExtensionContext, private config: Config) { 14 | registerCommand(context, 'extension.createTrelloCard', this.createTrelloCard.bind(this)); 15 | } 16 | 17 | updateConfiguration(config: Config) { 18 | this.config = config; 19 | } 20 | 21 | @TrackFeature('CreateCard') 22 | async createTrelloCard(item: ActionComment) { 23 | const name = `${item.commentType}: ${item.label}`; 24 | const desc = `[View File](${this.config.scheme}://TzachOvadia.todo-list/view?file=${encodeURIComponent(item.uri.fsPath)}#${item.position})`; 25 | 26 | let key = 'a20752c7ff035d5001ce2938f298be64'; 27 | let token = this.config.trello.token; 28 | let listId = this.config.trello.defaultList; 29 | 30 | if (!token) { 31 | try { 32 | token = await this.getToken(key); 33 | vscode.workspace.getConfiguration('trello').update('token', token, vscode.ConfigurationTarget.Global); 34 | } catch (e) { 35 | return; 36 | } 37 | } 38 | 39 | if (!listId) { 40 | const list = await this.selectTrelloList(key, token); 41 | if (!list) { 42 | return; 43 | } 44 | listId = list.id; 45 | 46 | vscode.workspace.getConfiguration('trello').update('defaultList', listId, vscode.ConfigurationTarget.Global); 47 | } 48 | 49 | const addCardResult = await this.addTrelloCard(listId, name, desc, key, token); 50 | } 51 | 52 | async selectTrelloList(key: string, token: string) { 53 | try { 54 | const boards = await this.getTrelloBoards(key, token); 55 | if (!boards) { 56 | return; 57 | } 58 | 59 | const selectedBoard = await vscode.window.showQuickPick(boards.map(p => ({ ...p, label: p.name })), { placeHolder: 'Select Trello Board' }); 60 | if (!selectedBoard) { 61 | return; 62 | } 63 | 64 | const lists = await this.getTrelloLists(selectedBoard.id, key, token); 65 | if (!lists) { 66 | return; 67 | } 68 | 69 | const selectedList = await vscode.window.showQuickPick(lists.map(p => ({ ...p, label: p.name })), { placeHolder: 'Select List' }); 70 | return selectedList; 71 | 72 | } catch (e) { 73 | console.error(e); 74 | return; 75 | } 76 | } 77 | 78 | private getToken(key: string) { 79 | return new Promise(async (resolve, reject) => { 80 | let token = await vscode.window.showInputBox({ prompt: 'Trello Token', ignoreFocusOut: true }); 81 | if (!!token) { 82 | return resolve(token); 83 | } 84 | 85 | const genToken = await vscode.window.showInformationMessage( 86 | 'Trello token is required in order to create new cards. Click `Generate` to open authorization page.', 87 | { modal: false }, 88 | { title: 'Generate' }); 89 | if (!genToken) { 90 | return reject(); 91 | } 92 | const listener = vscode.window.onDidChangeWindowState(async e => { 93 | if (e.focused) { 94 | const value = await clipboardy.read(); 95 | token = await vscode.window.showInputBox({ prompt: 'Trello Token', ignoreFocusOut: true, value: value }); 96 | 97 | listener.dispose(); 98 | if (!!token) { 99 | return resolve(token); 100 | } 101 | } 102 | }); 103 | 104 | await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`https://trello.com/1/authorize?name=TODO%20List&scope=read,write&expiration=never&response_type=token&key=${key}`)); 105 | }); 106 | } 107 | 108 | private getTrelloBoards(key: string, token: string): Promise { 109 | return httpGet(`https://api.trello.com/1/members/me/boards?key=${key}&token=${token}`); 110 | } 111 | 112 | private getTrelloLists(boardId: string, key: string, token: string) { 113 | return httpGet(`https://api.trello.com/1/boards/${boardId}/lists?key=${key}&token=${token}`); 114 | 115 | } 116 | 117 | private addTrelloCard(listId: string, name: string, desc: string, key: string, token: string) { 118 | return httpPost(`/1/cards?idList=${listId}&name=${encodeURIComponent(name)}&desc=${encodeURIComponent(desc)}&keepFromSource=all&key=${key}&token=${token}`); 119 | } 120 | } 121 | 122 | function httpGet(url: string): Promise { 123 | return new Promise((resolve, reject) => { 124 | https.get(url, (res: http.IncomingMessage) => { 125 | res.setEncoding('utf8'); 126 | let rawData = ''; 127 | res.on('data', (chunk) => { rawData += chunk; }); 128 | res.on('end', () => { 129 | try { 130 | const parsedData = JSON.parse(rawData); 131 | resolve(parsedData); 132 | } catch (e) { 133 | reject(e); 134 | } 135 | }); 136 | }).on('error', (e) => reject(e)); 137 | }); 138 | } 139 | 140 | function httpPost(urlPath: string) { 141 | return new Promise((resolve, reject) => { 142 | const postData = ''; 143 | 144 | const options: http.RequestOptions = { 145 | hostname: 'api.trello.com', 146 | port: 443, 147 | path: urlPath, 148 | method: 'POST', 149 | headers: { 150 | 'Content-Type': 'application/json', 151 | 'Content-Length': Buffer.byteLength(postData) 152 | } 153 | }; 154 | 155 | const req = https.request(options, (res) => { 156 | res.setEncoding('utf8'); 157 | let data = ''; 158 | res.on('data', (chunk) => { 159 | data += chunk; 160 | }); 161 | res.on('end', () => { 162 | resolve(data); 163 | }); 164 | }); 165 | 166 | req.on('error', (e) => { 167 | reject(e); 168 | }); 169 | 170 | // write data to request body 171 | req.write(postData); 172 | req.end(); 173 | }); 174 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-list", 3 | "displayName": "TODO List", 4 | "version": "1.6.0", 5 | "description": "List all action comments (TODO, FIXME, HACK etc) in an easy-to-read list", 6 | "scripts": { 7 | "vscode:prepublish": "tsc -p ./", 8 | "compile": "tsc -p ./", 9 | "compile:watch": "tsc -watch -p ./", 10 | "postinstall": "node ./node_modules/vscode/bin/install" 11 | }, 12 | "author": "Tzach Ovadia", 13 | "license": "ISC", 14 | "publisher": "TzachOvadia", 15 | "icon": "icons/icon.png", 16 | "galleryBanner": { 17 | "color": "#c3cdd4", 18 | "theme": "light" 19 | }, 20 | "engines": { 21 | "vscode": "^1.26.0" 22 | }, 23 | "enableProposedApi": true, 24 | "categories": [ 25 | "Other", 26 | "Formatters" 27 | ], 28 | "keywords": [ 29 | "todo", 30 | "hack", 31 | "fixme", 32 | "list", 33 | "trello", 34 | "gmail", 35 | "comment", 36 | "tree", 37 | "view", 38 | "task", 39 | "highlight", 40 | "format" 41 | ], 42 | "activationEvents": [ 43 | "*" 44 | ], 45 | "preview": false, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/tzachov/vscode-todo-list" 49 | }, 50 | "main": "./out/src/extension", 51 | "devDependencies": { 52 | "typescript": "^2.0.3", 53 | "vscode": "^1.1.29", 54 | "@types/node": "*", 55 | "@types/micromatch": "3.1.0", 56 | "tslib": "^1.9.3" 57 | }, 58 | "dependencies": { 59 | "applicationinsights": "^1.3.1", 60 | "clipboardy": "^1.2.3", 61 | "cryptr": "~4.0.2", 62 | "fs": "^0.0.2", 63 | "micromatch": "^4.0.2", 64 | "parse-git-config": "^3.0.0", 65 | "path": "0.12.7" 66 | }, 67 | "contributes": { 68 | "commands": [ 69 | { 70 | "command": "extension.contextMenu", 71 | "title": "TODO Context Menu" 72 | }, 73 | { 74 | "command": "extension.collapseAll", 75 | "title": "Collapse All", 76 | "category": "TODO List", 77 | "icon": { 78 | "dark": "icons/dark/collapse-all.svg", 79 | "light": "icons/light/collapse-all.svg" 80 | } 81 | }, 82 | { 83 | "command": "extension.refreshActionComments", 84 | "title": "Refresh List", 85 | "category": "TODO List", 86 | "icon": { 87 | "dark": "icons/dark/refresh.svg", 88 | "light": "icons/light/refresh.svg" 89 | } 90 | }, 91 | { 92 | "command": "extension.removeActionComment", 93 | "title": "Remove", 94 | "category": "TODO List", 95 | "icon": { 96 | "dark": "icons/dark/close.svg", 97 | "light": "icons/light/close.svg" 98 | } 99 | }, 100 | { 101 | "command": "extension.viewComment", 102 | "title": "View Comment", 103 | "category": "TODO List" 104 | }, 105 | { 106 | "command": "extension.editComment", 107 | "title": "Edit", 108 | "category": "TODO List" 109 | }, 110 | { 111 | "command": "extension.insertComment", 112 | "title": "Insert Comment", 113 | "category": "TODO List" 114 | }, 115 | { 116 | "command": "extension.createTrelloCard", 117 | "title": "Create Trello Card", 118 | "category": "TODO List" 119 | }, 120 | { 121 | "command": "extension.createGitHubIssue", 122 | "title": "Create GitHub Issue", 123 | "category": "TODO List" 124 | }, 125 | { 126 | "command": "extension.sendUsingGmail", 127 | "title": "Send using Gmail", 128 | "category": "TODO List" 129 | }, 130 | { 131 | "command": "extension.copyComment", 132 | "title": "Copy", 133 | "category": "TODO List" 134 | }, 135 | { 136 | "command": "extension.convertToComment", 137 | "title": "Convert to comment", 138 | "category": "TODO List" 139 | } 140 | ], 141 | "menus": { 142 | "commandPalette": [ 143 | { 144 | "command": "extension.contextMenu", 145 | "when": "false" 146 | }, 147 | { 148 | "command": "extension.collapseAll", 149 | "when": "false" 150 | }, 151 | { 152 | "command": "extension.refreshActionComments" 153 | }, 154 | { 155 | "command": "extension.removeActionComment", 156 | "when": "false" 157 | }, 158 | { 159 | "command": "extension.viewComment", 160 | "when": "false" 161 | }, 162 | { 163 | "command": "extension.editComment", 164 | "when": "false" 165 | }, 166 | { 167 | "command": "extension.copyComment", 168 | "when": "false" 169 | }, 170 | { 171 | "command": "extension.convertToComment", 172 | "when": "false" 173 | }, 174 | { 175 | "command": "extension.sendUsingGmail", 176 | "when": "false" 177 | }, 178 | { 179 | "command": "extension.createTrelloCard", 180 | "when": "false" 181 | }, 182 | { 183 | "command": "extension.createGitHubIssue", 184 | "when": "false" 185 | }, 186 | { 187 | "command": "extension.insertComment" 188 | } 189 | ], 190 | "view/item/context": [ 191 | { 192 | "command": "extension.removeActionComment", 193 | "when": "view == actionComments && viewItem == $Comment", 194 | "group": "inline" 195 | }, 196 | { 197 | "command": "extension.viewComment", 198 | "when": "view == actionComments && viewItem == $Comment", 199 | "group": "3_open" 200 | }, 201 | { 202 | "command": "extension.removeActionComment", 203 | "when": "view == actionComments && viewItem == $Comment", 204 | "group": "7_modification" 205 | }, 206 | { 207 | "command": "extension.editComment", 208 | "when": "view == actionComments && viewItem == $Comment", 209 | "group": "9_cutcopypaste" 210 | }, 211 | { 212 | "command": "extension.copyComment", 213 | "when": "view == actionComments && viewItem == $Comment", 214 | "group": "9_cutcopypaste" 215 | }, 216 | { 217 | "command": "extension.createTrelloCard", 218 | "when": "view == actionComments && viewItem == $Comment", 219 | "group": "z_commands" 220 | }, 221 | { 222 | "command": "extension.sendUsingGmail", 223 | "when": "view == actionComments && viewItem == $Comment", 224 | "group": "z_commands" 225 | }, 226 | { 227 | "command": "extension.createGitHubIssue", 228 | "when": "view == actionComments && viewItem == $Comment", 229 | "group": "z_commands" 230 | } 231 | ], 232 | "view/title": [ 233 | { 234 | "command": "extension.refreshActionComments", 235 | "when": "view == actionComments", 236 | "group": "navigation" 237 | } 238 | ] 239 | }, 240 | "keybindings": [ 241 | { 242 | "command": "extension.insertComment", 243 | "key": "ctrl+shift+a", 244 | "mac": "cmd+shift+a", 245 | "when": "textInputFocus && !editorReadonly" 246 | } 247 | ], 248 | "views": { 249 | "explorer": [ 250 | { 251 | "id": "actionComments", 252 | "name": "Action Comments" 253 | } 254 | ] 255 | }, 256 | "configuration": { 257 | "title": "TODO List", 258 | "properties": { 259 | "expression": { 260 | "type": "string", 261 | "default": "(?:\\/\\/|\\/\\*|\\<\\!--)[ ]?([A-Z]+)(?: |\\:|\\(([A-Za-z\\/\\d ]+)\\)\\:)[ ]?(.*?)[ ]?(?:--\\>|\\*\\/|$)", 262 | "markdownDescription": "RegExp to use for extracting comments (first group must be type, last must be text)\nWe recommend capturing only all-uppercase types to avoid capturing `tslint:` and commented properties" 263 | }, 264 | "scanOnSave": { 265 | "type": "boolean", 266 | "default": true, 267 | "description": "Scan comments when saving a file" 268 | }, 269 | "include": { 270 | "type": "string", 271 | "default": "**/*.{ts,js,php,css,scss,html}", 272 | "description": "Glob pattern to include in scans" 273 | }, 274 | "exclude": { 275 | "type": "string", 276 | "default": "{**/node_modules/**,**/bower_components/**,**/dist/**,**/build/**,**/.vscode/**,**/_output/**,**/vendor/**,**/*.min.*,**/*.map}", 277 | "description": "Glob pattern to exclude from scans" 278 | }, 279 | "name": { 280 | "type": "string", 281 | "default": null, 282 | "markdownDescription": "Name to use as `Created by`" 283 | }, 284 | "enableCommentFormatting": { 285 | "type": "boolean", 286 | "default": true, 287 | "description": "Enable comment formatting (Set color for comment type and make text italic)" 288 | }, 289 | "enableTelemetry": { 290 | "type": "boolean", 291 | "default": null, 292 | "description": "Enable anonymous data collection (only collects extension started and extension actions used) - No personal or project data is sent" 293 | }, 294 | "trello.token": { 295 | "type": "string", 296 | "default": null, 297 | "markdownDescription": "In order to create Trello cards, this extension requires read and write permissions.\n\n[Click here to generate token](https://trello.com/1/authorize?name=TODO%20List&scope=read,write&expiration=never&response_type=token&key=a20752c7ff035d5001ce2938f298be64)." 298 | }, 299 | "trello.defaultList": { 300 | "type": "string", 301 | "default": null, 302 | "description": "List ID to create cards in (will be automatically set on first use)" 303 | }, 304 | "github.auth": { 305 | "type": "string", 306 | "default": null, 307 | "description": "Encrypted credentials for GitHub operations" 308 | }, 309 | "github.storeCredentials": { 310 | "type": "boolean", 311 | "default": true, 312 | "description": "Store encrypted GitHub credentials" 313 | }, 314 | "actionTypes": { 315 | "type": "string", 316 | "default": "TODO,FIXME,HACK,BUG,UNDONE", 317 | "description": "Comma-separated action types" 318 | }, 319 | "enabledCodeActions": { 320 | "type": "boolean", 321 | "default": false, 322 | "description": "Enable CodeActions (lightbulb) menu in supported files" 323 | } 324 | } 325 | } 326 | } 327 | } 328 | --------------------------------------------------------------------------------