Skip to content

Commit d6f17ed

Browse files
committed
[gtd_helper plugin] GTD helper is plugin to support GTD workflow in Vimflowly.
Create a node anywhere with "GTD" text in it and started adding your gtd tags. Adding tags like #today, #next, #soon gets automatically cloned in GTD node with their topics on calling the trigger keyboard shortcut.
1 parent a70b24e commit d6f17ed

File tree

3 files changed

+233
-0
lines changed

3 files changed

+233
-0
lines changed

src/assets/ts/plugins.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ export class PluginApi {
200200
Please report this problem to the plugin author.`
201201
);
202202
}
203+
204+
public showAlert(message: string) {
205+
alert(message);
206+
}
203207
}
204208

205209
export class PluginsManager extends EventEmitter {

src/plugins/gtd_helper/index.tsx

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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+
);

src/plugins/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import './text_formatting';
66
import './time_tracking';
77
import './todo';
88
import './recursive_expand';
9+
import './gtd_helper';
910

1011
// for developers: uncomment the following lines
1112
/*

0 commit comments

Comments
 (0)