Skip to content

Commit cd744e6

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 cd744e6

File tree

3 files changed

+232
-0
lines changed

3 files changed

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

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)