|
| 1 | +import * as React from 'react'; // tslint:disable-line no-unused-variable |
| 2 | + |
| 3 | +// tslint:disable-next-line:max-line-length |
| 4 | +import { registerPlugin, PluginApi } from '../../assets/ts/plugins'; |
| 5 | +import { Logger } from '../../shared/utils/logger'; |
| 6 | +import { matchWordRegex } from '../../assets/ts/utils/text'; |
| 7 | +import Session from '../../assets/ts/session'; |
| 8 | +import * as _ from 'lodash'; |
| 9 | +import { Row, SerializedBlock } from '../../assets/ts/types'; |
| 10 | +import Path from '../../assets/ts/path'; |
| 11 | + |
| 12 | +// TODO: |
| 13 | +// - move all from _getChildren to getChildren which is based on path rather than row. |
| 14 | +// - use line renderHooks to automatically detect if GTD tag is present or not and clone it. |
| 15 | +// - Support link to original parent in gtd tasks. |
| 16 | +class GtdHelperPlugin { |
| 17 | + private api: PluginApi; |
| 18 | + private logger: Logger; |
| 19 | + private gtdRegex: RegExp; |
| 20 | + private loggerPrefix: string; |
| 21 | + private gtdContainedRows: _.Dictionary<Set<any>>; |
| 22 | + private clonedRows: Set<Row>; |
| 23 | + private gtdNodeRow: Row; |
| 24 | + private nodesUpdatedCounter: number; |
| 25 | + |
| 26 | + |
| 27 | + constructor(api: PluginApi) { |
| 28 | + this.api = api; |
| 29 | + this.logger = this.api.logger; |
| 30 | + this.gtdRegex = matchWordRegex('\\#\\w+?'); |
| 31 | + this.loggerPrefix = 'GTD Plugin: '; |
| 32 | + this.gtdContainedRows = {}; |
| 33 | + this.clonedRows = new Set<Row>(); |
| 34 | + this.gtdNodeRow = -1; |
| 35 | + this.nodesUpdatedCounter = 0; |
| 36 | + |
| 37 | + const that = this; |
| 38 | + this.api.registerAction( |
| 39 | + 'generate-gtd-tasks', |
| 40 | + 'Generates GTD tasks for the document', |
| 41 | + async function ({ session }) { |
| 42 | + await that.processGtdAction(session); |
| 43 | + }, |
| 44 | + ); |
| 45 | + |
| 46 | + |
| 47 | + this.api.registerDefaultMappings( |
| 48 | + 'NORMAL', |
| 49 | + { |
| 50 | + 'generate-gtd-tasks': [['ctrl+t']], |
| 51 | + }, |
| 52 | + ); |
| 53 | + } |
| 54 | + |
| 55 | + private log(message: any) { |
| 56 | + this.logger.info(this.loggerPrefix + message.toString()); |
| 57 | + } |
| 58 | + |
| 59 | + private async getChildWithText(row: Row, text: string): Promise<Row> { |
| 60 | + if (await this.api.session.document.hasChildren(row)) { |
| 61 | + let children = await this.api.session.document._getChildren(row); |
| 62 | + |
| 63 | + let foundRow: Row = -1; |
| 64 | + await Promise.all(children.map(async (child_row) => { |
| 65 | + let child_text = await this.api.session.document.getText(child_row); |
| 66 | + if (child_text.toLowerCase() === text.toLowerCase()) { |
| 67 | + foundRow = child_row; |
| 68 | + } |
| 69 | + })); |
| 70 | + |
| 71 | + return foundRow; |
| 72 | + } |
| 73 | + return -1; |
| 74 | + } |
| 75 | + |
| 76 | + private async getGtdNode() { |
| 77 | + this.gtdNodeRow = await this.getChildWithText(this.api.session.document.root.row, 'gtd'); |
| 78 | + } |
| 79 | + |
| 80 | + private async processGtdAction(session: Session) { |
| 81 | + this.gtdContainedRows = {}; |
| 82 | + this.clonedRows = new Set<Row>(); |
| 83 | + this.nodesUpdatedCounter = 0; |
| 84 | + |
| 85 | + await this.getGtdNode(); |
| 86 | + if (this.gtdNodeRow === -1) { |
| 87 | + this.api.showAlert('Create GTD node in root'); |
| 88 | + return; |
| 89 | + } |
| 90 | + |
| 91 | + await this.cleanAndFindCurrentGtdNodes(); |
| 92 | + await this.findAllGtdTaggedRows(session.document.root.row); |
| 93 | + await this.cloneGtdRows(); |
| 94 | + |
| 95 | + await this.api.session.showMessage('Number of nodes added/updated: ' + this.nodesUpdatedCounter); |
| 96 | + } |
| 97 | + |
| 98 | + private async findAllGtdTaggedRows(row: Row) { |
| 99 | + if (row === this.gtdNodeRow) { |
| 100 | + // don't parse gtd content. |
| 101 | + return; |
| 102 | + } |
| 103 | + let text = await this.api.session.document.getText(row); |
| 104 | + let match = this.gtdRegex.exec(text); |
| 105 | + if (match) { |
| 106 | + // only process if it's not a clone. |
| 107 | + // if it's a clone then it's already being tracked. |
| 108 | + let isClone = await this.api.session.document.isClone(row); |
| 109 | + if (!isClone) { |
| 110 | + let gtdKeyword = match[1]; |
| 111 | + this.log('found hashed on ' + row + ' ' + gtdKeyword); |
| 112 | + if (this.gtdContainedRows[gtdKeyword] === undefined) { |
| 113 | + this.gtdContainedRows[gtdKeyword] = new Set<any>(); |
| 114 | + } |
| 115 | + this.gtdContainedRows[gtdKeyword].add(row); |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + if (await this.api.session.document.hasChildren(row)) { |
| 120 | + let children = await this.api.session.document._getChildren(row); |
| 121 | + |
| 122 | + await Promise.all(children.map(async (child_row) => { |
| 123 | + await this.findAllGtdTaggedRows(child_row); |
| 124 | + })); |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + // Takes care of removing un-tagged nodes in GTD and |
| 129 | + // also takes care of removing nodes who's parent doesn't match |
| 130 | + // the gtd topic. |
| 131 | + private async cleanAndFindCurrentGtdNodes() { |
| 132 | + let gtdTopicNodes = await this.api.session.document._getChildren(this.gtdNodeRow); |
| 133 | + |
| 134 | + await Promise.all(gtdTopicNodes.map(async (topicNodeRow) => { |
| 135 | + let topic = await this.api.session.document.getText(topicNodeRow); |
| 136 | + let gtdTaskNodes = await this.api.session.document._getChildren(topicNodeRow); |
| 137 | + |
| 138 | + gtdTaskNodes.forEach(async (child_row) => { |
| 139 | + let child_text = await this.api.session.document.getText(child_row); |
| 140 | + if (child_text.toLowerCase().indexOf(topic) === -1) { |
| 141 | + // contains the wrong gtd topic in the gtd task. Needs to be removed. |
| 142 | + const index = _.findIndex(gtdTaskNodes, sib => sib === child_row); |
| 143 | + await this.api.session.delBlocks(topicNodeRow, index, 1, {}); |
| 144 | + await this.api.updatedDataForRender(child_row); |
| 145 | + this.log('Deleting Cloned Node: ' + child_row + ' as it\'s gtd topic doesn\'t match ' + topic); |
| 146 | + this.nodesUpdatedCounter += 1; |
| 147 | + } else { |
| 148 | + this.clonedRows.add(child_row); |
| 149 | + } |
| 150 | + }); |
| 151 | + })); |
| 152 | + } |
| 153 | + |
| 154 | + private async cloneGtdRows() { |
| 155 | + const gtdPath = new Path(this.api.session.document.root, this.gtdNodeRow); |
| 156 | + if (gtdPath.parent == null) { |
| 157 | + throw new Error('Cursor was at root'); |
| 158 | + } |
| 159 | + |
| 160 | + for (let key in this.gtdContainedRows) { |
| 161 | + let gtdrows = this.gtdContainedRows[key]; |
| 162 | + let rowsToClone = new Set<any>(); |
| 163 | + for (let val of Array.from(gtdrows.values())) { |
| 164 | + if (this.clonedRows.has(Number(val))) { |
| 165 | + this.log('Skipping to clone row = ' + val + ' as it\'s already cloned'); |
| 166 | + } else { |
| 167 | + this.log('Need to clone row = ' + val); |
| 168 | + rowsToClone.add(val); |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + await this.addClonedRows(gtdPath, key, Array.from(rowsToClone.values())); |
| 173 | + } |
| 174 | + |
| 175 | + this.api.updatedDataForRender(this.gtdNodeRow); |
| 176 | + } |
| 177 | + |
| 178 | + private async addClonedRows(path: Path, key: string, rows: Array<Row>) { |
| 179 | + if (rows.length <= 0) { |
| 180 | + return; |
| 181 | + } |
| 182 | + |
| 183 | + let keyNodeRow = await this.getChildWithText(path.row, key); |
| 184 | + |
| 185 | + if (keyNodeRow === -1) { |
| 186 | + this.log(key + ' based node doesn\'t exist. Creating it'); |
| 187 | + |
| 188 | + // const index = await this.api.session.document.indexInParent(path); |
| 189 | + let serialzed_row: SerializedBlock = { |
| 190 | + text: key, |
| 191 | + collapsed: false, |
| 192 | + children: [], |
| 193 | + }; |
| 194 | + await this.api.session.addBlocks(path, 0, [serialzed_row], {}); |
| 195 | + } |
| 196 | + |
| 197 | + keyNodeRow = await this.getChildWithText(path.row, key); |
| 198 | + |
| 199 | + this.log('Cloning for key = ' + key + ' containing rows = ' + rows); |
| 200 | + this.api.session.attachBlocks(new Path(path, keyNodeRow), rows, 0, { setCursor: 'first' }); |
| 201 | + this.nodesUpdatedCounter += rows.length; |
| 202 | + await this.api.updatedDataForRender(path.row); |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | + |
| 207 | +registerPlugin( |
| 208 | + { |
| 209 | + name: 'GTD Helper', |
| 210 | + author: 'Nikhil Sonti', |
| 211 | + description: ( |
| 212 | + <div> |
| 213 | + GTD helper is plugin to support GTD workflow in Vimflowly. |
| 214 | + How to use: |
| 215 | + <ul> |
| 216 | + <li> Create a node anywhere with "GTD" text in it and started adding your gtd tags.</li> |
| 217 | + <li> Adding tags like #today, #next, #soon gets automatically cloned in |
| 218 | + GTD node with their topics on calling the trigger keyboard shortcut. </li> |
| 219 | + </ul> |
| 220 | + </div> |
| 221 | + ), |
| 222 | + }, |
| 223 | + async (api) => { |
| 224 | + const gtdHelper = new GtdHelperPlugin(api); |
| 225 | + return gtdHelper; |
| 226 | + }, |
| 227 | + (api => api.deregisterAll()), |
| 228 | +); |
0 commit comments