Skip to content

Tags plugin #364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 75 additions & 74 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,75 +1,76 @@
{
"name": "vimflowy",
"version": "0.1.0",
"private": true,
"homepage": "./",
"dependencies": {
"@types/file-saver": "^2.0.1",
"@types/jest": "^26.0.19",
"@types/node": "^12.19.11",
"@types/react-dom": "^16.9.10",
"@types/express": "^4.16.0",
"@types/sqlite3": "^2.2.33",
"@types/ws": "0.0.41",
"express": "^4.16.4",
"file-saver": "^2.0.5",
"firebase": "^8.2.1",
"font-awesome": "^4.7.0",
"jquery": "^3.5.1",
"katex": "^0.12.0",
"lodash": "^4.17.20",
"node-sass": "^4.14.1",
"react": "^17.0.1",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1",
"sqlite3": "^4.1.1",
"ts-node": "^3.3.0",
"typescript": "^4.1.3",
"web-vitals": "^0.2.4",
"ws": "^6.1.2"
},
"scripts": {
"lint": "tslint --exclude 'node_modules/**/*' '**/*.ts' '**/*.tsx'",
"startprod": "ts-node --compilerOptions '{\"module\":\"commonjs\"}' server/prod.ts",
"typecheck": "tsc -p . --noEmit",
"verify": "npm run lint && npm run typecheck && npm test",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --opts test/mocha.opts",
"watchtest": "npm test -- --reporter dot --watch",
"profiletest": "npm test -- --prof",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/jquery": "^3.5.5",
"@types/katex": "^0.11.0",
"@types/lodash": "^4.14.166",
"@types/minimist": "^1.2.0",
"@types/mocha": "^2.2.48",
"@types/react": "^16.14.2",
"@types/react-color": "^3.0.4",
"ignore-styles": "^5.0.1",
"minimist": "^1.2.2",
"mocha": "^5.2.0",
"tslint": "^4.5.1"
}
}
"name": "vimflowy",
"version": "0.1.0",
"private": true,
"homepage": "./",
"dependencies": {
"@types/file-saver": "^2.0.1",
"@types/jest": "^26.0.19",
"@types/node": "^12.19.11",
"@types/react-dom": "^16.9.10",
"@types/express": "^4.16.0",
"@types/sqlite3": "^2.2.33",
"@types/ws": "0.0.41",
"express": "^4.16.4",
"file-saver": "^2.0.5",
"firebase": "^8.2.1",
"font-awesome": "^4.7.0",
"jquery": "^3.5.1",
"katex": "^0.12.0",
"lodash": "^4.17.20",
"node-sass": "^4.14.1",
"react": "^17.0.1",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1",
"sqlite3": "^4.1.1",
"ts-node": "^3.3.0",
"typescript": "^4.1.3",
"web-vitals": "^0.2.4",
"ws": "^6.1.2"
},
"scripts": {
"lint": "tslint --exclude 'node_modules/**/*' '**/*.ts' '**/*.tsx'",
"startprod": "ts-node --compilerOptions '{\"module\":\"commonjs\"}' server/prod.ts",
"typecheck": "tsc -p . --noEmit",
"verify": "npm run lint && npm run typecheck && npm test",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --opts test/mocha.opts",
"test-windows": "SET TS_NODE_COMPILER_OPTIONS={\"module\":\"commonjs\"}&& mocha --opts test/mocha.opts",
"watchtest": "npm test -- --reporter dot --watch",
"profiletest": "npm test -- --prof",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/jquery": "^3.5.5",
"@types/katex": "^0.11.0",
"@types/lodash": "^4.14.166",
"@types/minimist": "^1.2.0",
"@types/mocha": "^2.2.48",
"@types/react": "^16.14.2",
"@types/react-color": "^3.0.4",
"ignore-styles": "^5.0.1",
"minimist": "^1.2.2",
"mocha": "^5.2.0",
"tslint": "^4.5.1"
}
}
7 changes: 7 additions & 0 deletions src/assets/ts/configurations/vim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,13 @@ function getDefaultData(): Array<SerializedBlock> {
'Link to marks with the @ symbol, like this: @im_a_mark. Use gm to follow the link.',
'Delete marks by using dm, or just mark with empty string',
] },
{ text: 'Tags', plugins: { tags: ['tag'] }, collapsed: true, children: [
{ text: 'I am tagged!', plugins: { tags: ['tag', 'another_tag'] } },
'Each row can have multiple tags, and rows can share the same tags',
'Press # to start adding a tag to a line, and enter to finish',
'Use - to search and jump to tags',
'Delete the i\'th tag by using d#i',
] },
{ text: 'Cloning', collapsed: true, children: [
{ text: 'I am a clone! Try editing me', id: 1 },
{ text: 'Clones can\'t be siblings or descendants of each other', children: [
Expand Down
2 changes: 1 addition & 1 deletion src/assets/ts/datastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ type DocSetting = keyof DocSettings;

const default_doc_settings: DocSettings = {
// TODO import these names from the plugins
enabledPlugins: ['Marks', 'HTML', 'LaTeX', 'Text Formatting', 'Todo'],
enabledPlugins: ['Marks', 'HTML', 'LaTeX', 'Text Formatting', 'Todo', 'Tags'],
};

const timeout = (ns: number) => {
Expand Down
159 changes: 159 additions & 0 deletions src/plugins/clone_tags/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as _ from 'lodash';
import * as React from 'react'; // tslint:disable-line no-unused-variable

import { Logger } from '../../shared/utils/logger';

import { registerPlugin, PluginApi } from '../../assets/ts/plugins';
import Path from '../../assets/ts/path';
import { Row, SerializedBlock } from '../../assets/ts/types';

import { pluginName as marksPluginName, MarksPlugin } from '../marks';
import { pluginName as tagsPluginName, TagsPlugin } from '../tags';

// TODO: do this elsewhere
declare const process: any;

type Tag = string;

/*
* ALGORITHMIC NOTE: maintaining the set of tags
* Rather than trying to update the list
* as rows get removed and added from the document (which is especially
* tricky because of cloning),
* we simply store all tags, even if attached to the document,
* and then prune after looking them up.
*/

export class CloneTagsPlugin {
private api: PluginApi;
private logger: Logger;
private tagRoot: {[tag: string]: Path | null};
private tagsPlugin: TagsPlugin;

constructor(api: PluginApi) {
this.api = api;
this.logger = this.api.logger;
this.tagRoot = {};
this.tagsPlugin = this.api.getPlugin(tagsPluginName) as TagsPlugin;
}

public async enable() {
this.logger.debug('Enabling cloning tags');

this.api.registerHook('document', 'tagAdded', async (_struct, { tag, row }) => {
await this.createClone(row, tag);
});

this.api.registerHook('document', 'tagRemoved', async (_struct, { tag, row }) => {
await this.deleteClone(row, tag);
});
}

public async inTagRoot(row: Row, tag: Tag) {
// check if row is in top level of tag root
const root = await this.getTagRoot(tag);
const document = this.api.session.document;
const info = await document.getInfo(row);
const parents = info.parentRows;
return parents.includes(root.row);
}

public async createClone(row: Row, tag: Tag) {
const root = await this.getTagRoot(tag);
if (!await this.inTagRoot(row, tag)) {
await this.api.session.attachBlocks(root, [row], 0);
await this.api.updatedDataForRender(row);
}
}

public async deleteClone(row: Row, tag: Tag) {
if (!await this.inTagRoot(row, tag)) {
return;
}
const root = await this.getTagRoot(tag);
const document = this.api.session.document;
if (!root) {
return;
}
await document._detach(row, root.row);
await this.api.updatedDataForRender(row);
}

private async getMarkPath(mark: string): Promise<Path | null> {
const marksPlugin = this.api.getPlugin(marksPluginName) as MarksPlugin;
const marks = await marksPlugin.listMarks();
return marks[mark];
}

public async getTagRoot(tag: Tag): Promise<Path> {
let root = this.tagRoot[tag];
if (root && await this.api.session.document.isValidPath(root) && await this.api.session.document.isAttached(root.row)) {
return root;
} else {
root = await this.getMarkPath(tag);
if (!root) {
await this.createTagRoot(tag);
root = await this.getMarkPath(tag);
if (!root) {
throw new Error('Error while creating node');
}
}
this.tagRoot[tag] = root;
return root;
}
}

private async setMark(path: Path, mark: string) {
const marksPlugin = this.api.getPlugin(marksPluginName) as MarksPlugin;
await marksPlugin.setMark(path.row, mark);
}

private async createBlock(path: Path, text: string, isCollapsed: boolean = true, plugins?: any) {
let serialzed_row: SerializedBlock = {
text: text,
collapsed: isCollapsed,
plugins: plugins,
children: [],
};
const paths = await this.api.session.addBlocks(path, 0, [serialzed_row]);
if (paths.length > 0) {
await this.api.updatedDataForRender(path.row);
return paths[0];
} else {
throw new Error('Error while creating block');
}
}

private async createTagRoot(tag: Tag) {
const path = await this.createBlock(this.api.session.document.root, tag);
if (path) {
await this.setMark(path, tag);
}
const tagsToRows = await this.tagsPlugin._getTagsToRows();
const document = this.api.session.document;
for (let row of tagsToRows[tag]) {
if (await document.isAttached(row)) {
await this.createClone(row, tag);
}
}
}
}

export const pluginName = 'Tag Clone';

registerPlugin<CloneTagsPlugin>(
{
name: pluginName,
author: 'Victor Tao',
description:
`Creates a root node for every tag with mark [TAGNAME]. Tagged rows are cloned to to this node.`,
version: 1,
dependencies: [tagsPluginName, marksPluginName],
},
async (api) => {
const clonetagsPlugin = new CloneTagsPlugin(api);
await clonetagsPlugin.enable();
return clonetagsPlugin;
},
(api) => api.deregisterAll(),
);
2 changes: 2 additions & 0 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import './todo';
import './recursive_expand';
import './daily_notes';
import './deadlines';
import './tags';
import './clone_tags';

// for developers: uncomment the following lines
/*
Expand Down
Loading