From f0ba8251fc177a54f97795c8bd5424b7d3e060ac Mon Sep 17 00:00:00 2001 From: Jeff Wu Date: Sat, 8 Apr 2017 17:17:10 -0700 Subject: [PATCH] todo plugin --- package.json | 1 + src/assets/js/configurations/vim.ts | 6 -- src/assets/js/keyDefinitions.ts | 1 + src/assets/js/modes.ts | 1 + src/assets/js/session.ts | 20 +++-- src/assets/js/settings.ts | 1 + src/plugins/marks/index.tsx | 4 +- src/plugins/text_formatting/index.tsx | 23 +++-- src/plugins/todo/index.sass | 3 + src/plugins/todo/index.tsx | 121 ++++++++++++++++++++++++++ test/mocha.opts | 1 + test/tests/todo.ts | 85 ++++++++++++++++++ 12 files changed, 245 insertions(+), 22 deletions(-) create mode 100644 src/plugins/todo/index.sass create mode 100644 src/plugins/todo/index.tsx create mode 100644 test/tests/todo.ts diff --git a/package.json b/package.json index cd4adf17..9f1ec441 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint-plugin-react": "^6.4.1", "express": "^4.14.0", "file-loader": "^0.9.0", + "ignore-styles": "^5.0.1", "mocha": "^3.0.2", "node-sass": "^3.8.0", "react-hot-loader": "^1.3.0", diff --git a/src/assets/js/configurations/vim.ts b/src/assets/js/configurations/vim.ts index dc058e14..583292f6 100644 --- a/src/assets/js/configurations/vim.ts +++ b/src/assets/js/configurations/vim.ts @@ -6,7 +6,6 @@ import { motionKey } from '../keyDefinitions'; import { SINGLE_LINE_MOTIONS } from '../definitions/motions'; import Config from '../config'; -// TODO: 'swap-case': [['~']] // TODO: 'next-sentence': [[')']] // TODO: 'prev-sentence': [['(']] @@ -111,7 +110,6 @@ export const NORMAL_MODE_MAPPINGS: HotkeyMapping = Object.assign({ 'swap-block-up': [['ctrl+k']], 'search-local': [['ctrl+/'], ['ctrl+f']], 'search-global': [['/']], - 'toggle-strikethrough': [['ctrl+enter']], 'export-file': [['ctrl+s']], 'zoom-prev-sibling': [['alt+k']], 'zoom-next-sibling': [['alt+j']], @@ -146,7 +144,6 @@ export const VISUAL_LINE_MODE_MAPPINGS: HotkeyMapping = Object.assign({ 'visual-line-yank-clone': [['Y']], 'visual-line-indent': [['>'], ['tab'], ['ctrl+l']], 'visual-line-unindent': [['<'], ['shift+tab'], ['ctrl+h']], - 'visual-line-toggle-strikethrough': [['ctrl+enter']], 'visual-line-swap-case': [['~']], }, NORMAL_MOTION_MAPPINGS); @@ -176,7 +173,6 @@ export const INSERT_MODE_MAPPINGS: HotkeyMapping = Object.assign({ 'indent-blocks': [['tab']], 'swap-block-down': [], 'swap-block-up': [], - 'toggle-strikethrough': [['ctrl+enter']], 'zoom-prev-sibling': [['alt+k']], 'zoom-next-sibling': [['alt+j']], 'zoom-in': [['ctrl+right']], @@ -225,8 +221,6 @@ export const WORKFLOWY_MODE_MAPPINGS: HotkeyMapping = Object.assign({ 'indent-blocks': [['tab']], 'swap-block-down': [['meta+shift+up']], 'swap-block-up': [['meta+shift+down']], - // NOTE: in workflowy, this also crosses out children - 'toggle-strikethrough': [['meta+enter']], 'zoom-prev-sibling': [], 'zoom-next-sibling': [], 'zoom-in': [], diff --git a/src/assets/js/keyDefinitions.ts b/src/assets/js/keyDefinitions.ts index 68b56649..a7aa56e1 100644 --- a/src/assets/js/keyDefinitions.ts +++ b/src/assets/js/keyDefinitions.ts @@ -45,6 +45,7 @@ export type ActionContext = { end_i: number, start: Path, end: Path, + selected: Array, parent: Path, num_rows: number, }, diff --git a/src/assets/js/modes.ts b/src/assets/js/modes.ts index bba87e36..0b93af42 100644 --- a/src/assets/js/modes.ts +++ b/src/assets/js/modes.ts @@ -227,6 +227,7 @@ registerMode({ start: children[index1], end: children[index2], parent: parent, + selected: children.slice(index1, index2 + 1), num_rows: (index2 - index1) + 1, }; return context; diff --git a/src/assets/js/session.ts b/src/assets/js/session.ts index 8531c36f..5c280f42 100644 --- a/src/assets/js/session.ts +++ b/src/assets/js/session.ts @@ -731,7 +731,10 @@ export default class Session extends EventEmitter { return await this.document.getLength(this.cursor.row); } - private async addChars(row: Row, col: Col, chars: Chars) { + public async addChars(row: Row, col: Col, chars: Chars) { + if (col < 0) { + col = (await this.document.getLength(row)) + col + 1; + } await this.do(new mutations.AddChars(row, col, chars)); } @@ -747,11 +750,12 @@ export default class Session extends EventEmitter { await this.addChars(this.cursor.row, col, chars); } - private async delChars(path: Path, col: Col, nchars: number, options: DelCharsOptions = {}) { - const n = await this.document.getLength(path.row); + public async delChars(row: Row, col: Col, nchars: number, options: DelCharsOptions = {}) { + const n = await this.document.getLength(row); let deleted: Chars = []; + if (col < 0) { col = n + col; } if ((n > 0) && (nchars > 0) && (col < n)) { - const mutation = new mutations.DelChars(path.row, col, nchars); + const mutation = new mutations.DelChars(row, col, nchars); await this.do(mutation); deleted = mutation.deletedChars; if (options.yank) { @@ -763,11 +767,11 @@ export default class Session extends EventEmitter { public async delCharsBeforeCursor(nchars: number, options: DelCharsOptions = {}) { nchars = Math.min(this.cursor.col, nchars); - return await this.delChars(this.cursor.path, this.cursor.col - nchars, nchars, options); + return await this.delChars(this.cursor.path.row, this.cursor.col - nchars, nchars, options); } public async delCharsAfterCursor(nchars: number, options: DelCharsOptions = {}) { - return await this.delChars(this.cursor.path, this.cursor.col, nchars, options); + return await this.delChars(this.cursor.path.row, this.cursor.col, nchars, options); } private async changeChars(row: Row, col: Col, nchars: number, change_fn: (chars: Chars) => Chars) { @@ -817,7 +821,7 @@ export default class Session extends EventEmitter { // yank as a row, not chars await this.yankRowAtCursor(); } - return await this.delChars(this.cursor.path, 0, await this.curLineLength()); + return await this.delChars(this.cursor.path.row, 0, await this.curLineLength()); } public async yankChars(path: Path, col: Col, nchars: number) { @@ -860,7 +864,7 @@ export default class Session extends EventEmitter { [cursor1, cursor2] = [cursor2, cursor1]; } const offset = options.includeEnd ? 1 : 0; - await this.delChars(cursor1.path, cursor1.col, cursor2.col - cursor1.col + offset, options); + await this.delChars(cursor1.path.row, cursor1.col, cursor2.col - cursor1.col + offset, options); } public async newLineBelow( diff --git a/src/assets/js/settings.ts b/src/assets/js/settings.ts index afe0889f..2fa36ab1 100644 --- a/src/assets/js/settings.ts +++ b/src/assets/js/settings.ts @@ -21,6 +21,7 @@ const default_settings: SettingsType = { theme: 'default-theme', showKeyBindings: true, hotkeys: {}, + // TODO import these names from the plugins enabledPlugins: ['Marks', 'HTML', 'LaTeX', 'Text Formatting', 'Todo'], }; diff --git a/src/plugins/marks/index.tsx b/src/plugins/marks/index.tsx index 04255a00..512970c9 100644 --- a/src/plugins/marks/index.tsx +++ b/src/plugins/marks/index.tsx @@ -561,7 +561,7 @@ export class MarksPlugin { // NOTE: because listing marks filters, disabling is okay -const pluginName = 'Marks'; +export const pluginName = 'Marks'; registerPlugin( { @@ -578,5 +578,3 @@ registerPlugin( }, (api) => api.deregisterAll(), ); - -export { pluginName }; diff --git a/src/plugins/text_formatting/index.tsx b/src/plugins/text_formatting/index.tsx index a66816df..041934b9 100644 --- a/src/plugins/text_formatting/index.tsx +++ b/src/plugins/text_formatting/index.tsx @@ -30,15 +30,28 @@ registerPlugin( return tokenizer; } return tokenizer.then(RegexTokenizerModifier( - matchWordRegex('\\*\\*(\\n|.)+?\\*\\*'), - hideBorderAndModify(2, 2, (char_info) => { char_info.renderOptions.classes[boldClass] = true; }) + // triple asterisk means both bold and italic + matchWordRegex('\\*\\*\\*(\\n|.)+?\\*\\*\\*'), + hideBorderAndModify(3, 3, (char_info) => { + char_info.renderOptions.classes[italicsClass] = true; + char_info.renderOptions.classes[boldClass] = true; + }) )).then(RegexTokenizerModifier( // middle is either a single character, or both sides have a non-* character matchWordRegex('\\*((\\n|[^\\*])|[^\\*](\\n|.)+?[^\\*])?\\*'), - hideBorderAndModify(1, 1, (char_info) => { char_info.renderOptions.classes[italicsClass] = true; }) + hideBorderAndModify(1, 1, (char_info) => { + char_info.renderOptions.classes[italicsClass] = true; + }) + )).then(RegexTokenizerModifier( + matchWordRegex('\\*\\*(\\n|.)+?\\*\\*'), + hideBorderAndModify(2, 2, (char_info) => { + char_info.renderOptions.classes[boldClass] = true; + }) )).then(RegexTokenizerModifier( - matchWordRegex('_(\\n|.)+?_'), - hideBorderAndModify(1, 1, (char_info) => { char_info.renderOptions.classes[underlineClass] = true; }) + matchWordRegex('(?:[\\*]*)_(\\n|.)+?_(?:[\\*]*)'), + hideBorderAndModify(1, 1, (char_info) => { + char_info.renderOptions.classes[underlineClass] = true; + }) )); }); }, diff --git a/src/plugins/todo/index.sass b/src/plugins/todo/index.sass new file mode 100644 index 00000000..1d62556e --- /dev/null +++ b/src/plugins/todo/index.sass @@ -0,0 +1,3 @@ +.strikethrough + text-decoration: line-through + opacity: 0.5 diff --git a/src/plugins/todo/index.tsx b/src/plugins/todo/index.tsx new file mode 100644 index 00000000..03b339b5 --- /dev/null +++ b/src/plugins/todo/index.tsx @@ -0,0 +1,121 @@ +import * as _ from 'lodash'; + +import './index.sass'; + +import { hideBorderAndModify, RegexTokenizerModifier } from '../../assets/js/utils/token_unfolder'; +import { registerPlugin } from '../../assets/js/plugins'; +import { matchWordRegex } from '../../assets/js/utils'; +import { Row } from '../../assets/js/types'; +import Session from '../../assets/js/session'; + +const strikethroughClass = 'strikethrough'; + +export const pluginName = 'Todo'; + +registerPlugin( + { + name: pluginName, + author: 'Jeff Wu', + description: `Lets you strike out bullets`, + }, + function(api) { + api.registerHook('session', 'renderLineTokenHook', (tokenizer, hooksInfo) => { + if (hooksInfo.has_cursor) { + return tokenizer; + } + if (hooksInfo.has_highlight) { + return tokenizer; + } + return tokenizer.then(RegexTokenizerModifier( + matchWordRegex('\\~\\~(\\n|.)+?\\~\\~'), + hideBorderAndModify(2, 2, (char_info) => { char_info.renderOptions.classes[strikethroughClass] = true; }) + )); + }); + + async function isStruckThrough(session: Session, row: Row) { + const text = await session.document.getText(row); + return (text.slice(0, 2) === '~~') && (text.slice(-2) === '~~'); + } + + async function addStrikeThrough(session: Session, row: Row) { + await session.addChars(row, -1, ['~', '~']); + await session.addChars(row, 0, ['~', '~']); + } + + async function removeStrikeThrough(session: Session, row: Row) { + await session.delChars(row, -2, 2); + await session.delChars(row, 0, 2); + } + + api.registerAction( + 'toggle-strikethrough', + 'Toggle strikethrough for a row', + async function({ session }) { + if (await isStruckThrough(session, session.cursor.row)) { + await removeStrikeThrough(session, session.cursor.row); + } else { + await addStrikeThrough(session, session.cursor.row); + } + }, + ); + + // TODO: this should maybe strikethrough children, since UI suggests it? + api.registerAction( + 'visual-line-toggle-strikethrough', + 'Toggle strikethrough for rows', + async function({ session, visual_line }) { + if (visual_line == null) { + throw new Error('Visual_line mode arguments missing'); + } + + const is_struckthrough = await Promise.all( + visual_line.selected.map(async (path) => { + return await isStruckThrough(session, path.row); + }) + ); + if (_.every(is_struckthrough)) { + await Promise.all( + visual_line.selected.map(async (path) => { + await removeStrikeThrough(session, path.row); + }) + ); + } else { + await Promise.all( + visual_line.selected.map(async (path, i) => { + if (!is_struckthrough[i]) { + await addStrikeThrough(session, path.row); + } + }) + ); + } + await session.setMode('NORMAL'); + }, + ); + + api.registerDefaultMappings( + 'NORMAL', + { + 'toggle-strikethrough': [['ctrl+enter']], + }, + ); + + api.registerDefaultMappings( + 'INSERT', + { + 'toggle-strikethrough': [['ctrl+enter', 'meta+enter']], + }, + ); + + api.registerDefaultMappings( + 'VISUAL_LINE', + { + 'visual-line-toggle-strikethrough': [['ctrl+enter']], + }, + ); + + // TODO for workflowy mode + // NOTE: in workflowy, this also crosses out children + // 'toggle-strikethrough': [['meta+enter']], + }, + (api => api.deregisterAll()), +); diff --git a/test/mocha.opts b/test/mocha.opts index 1f101678..908b6cab 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,5 @@ --require ts-node/register +--require ignore-styles --watch-extensions tsx,ts --timeout 60000 test/tests/*.ts diff --git a/test/tests/todo.ts b/test/tests/todo.ts new file mode 100644 index 00000000..01248122 --- /dev/null +++ b/test/tests/todo.ts @@ -0,0 +1,85 @@ +/* globals describe, it */ +import TestCase from '../testcase'; +import * as Todo from '../../src/plugins/todo'; +import '../../src/assets/js/plugins'; + +const toggleStrikethroughKey = 'ctrl+enter'; + +describe('todo', function() { + it('works in basic case', async function() { + let t = new TestCase([ + 'a line', + 'another line', + ], {plugins: [Todo.pluginName]}); + t.sendKey(toggleStrikethroughKey); + t.expect([ + '~~a line~~', + 'another line', + ]); + + t.sendKey(toggleStrikethroughKey); + t.expect([ + 'a line', + 'another line', + ]); + + t.sendKey('u'); + t.expect([ + '~~a line~~', + 'another line', + ]); + + t.sendKey('u'); + t.expect([ + 'a line', + 'another line', + ]); + await t.done(); + }); + + it('works in visual line', async function() { + let t = new TestCase([ + 'a line', + '~~another line~~', + ], {plugins: [Todo.pluginName]}); + t.sendKeys('Vj'); + t.sendKey(toggleStrikethroughKey); + t.expect([ + '~~a line~~', + '~~another line~~', + ]); + + t.sendKeys('Vk'); + t.sendKey(toggleStrikethroughKey); + t.expect([ + 'a line', + 'another line', + ]); + + t.sendKeys('Vj'); + t.sendKey(toggleStrikethroughKey); + t.expect([ + '~~a line~~', + '~~another line~~', + ]); + + t.sendKey('u'); + t.expect([ + 'a line', + 'another line', + ]); + + t.sendKey('u'); + t.expect([ + '~~a line~~', + '~~another line~~', + ]); + + t.sendKey('u'); + t.expect([ + 'a line', + '~~another line~~', + ]); + await t.done(); + }); +});